using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; using HarmonyLib; using NeoModLoader.services; using UnityEngine; namespace IdleSpectator; /// /// Agent-driven in-game command bus. Drop JSON lines into .harness/commands.jsonl /// (or a scenario via {"action":"scenario","value":"smoke"}). Results go to /// .harness/results.jsonl and .harness/last-result.json. /// public static class AgentHarness { private static long _offset; private static float _waitUntil = -1f; private static bool _readyLogged; private static int _assertPass; private static int _assertFail; private static int _cmdOk; private static int _cmdFail; private static string _lastBatchId = ""; private static readonly Queue Queue = new Queue(); private static Actor _lastSpawned; private static string _lastSpawnedAssetId = ""; private static Actor _happinessPartner; private static long _activitySubjectId; private static bool _directorRunActive; /// /// When set, batch-end skips roster/memory wipe so AFK soak probes stay non-destructive. /// Scenario batches never set this; only soak_probe / preserve snapshots do. /// private static bool _preserveSagaAfterBatch; private static string _rememberedFocusKey = ""; private static string _rememberedTip = ""; private static long _rememberedRelatedId; private static string LastScreenshotPath = ""; private static Vector2 _sagaPanelSizeAnchor; private static string _captionBeatAnchor = ""; private static readonly string[] PreferredSpawnAssets = { "sheep", "cow", "pig", "chicken", "dog", "cat", "rabbit", "monkey", "fox", "crab" }; [Serializable] private class LastResult { public string batch_id = ""; public string status = ""; public int ok; public int fail; public int assert_pass; public int assert_fail; public string idle = ""; public string focus = ""; public string power_bar = ""; public string unit = ""; public string tip = ""; public string detail = ""; public string updated_at = ""; } public static bool Busy => Queue.Count > 0 || _waitUntil > 0f; /// /// When true, relationship interest feeds still Emit while /// (harness verification of live Harmony patches). /// public static bool ForceRelationshipFeeds; /// /// When true, event feeds/patches still Emit while /// for live verification (traits, WorldLog, wars, plots, books, meta, …). /// public static bool ForceLiveEventFeeds; /// True when production feeds may register during harness Busy. public static bool LiveFeedsAllowed => !Busy || ForceLiveEventFeeds || ForceRelationshipFeeds; /// /// When true, InterestDirector / SpeciesDiscovery stay frozen during harness commands. /// director_run temporarily clears this so dwell/interrupt/discovery can execute. /// public static bool FreezeDirector => Busy && !_directorRunActive; public static void Update() { EnsureDirs(); CheckExternalReset(); IngestNewCommands(); // Welcome Back / other ScrollWindows block world start until closed. if (ShouldAutoDismissUi()) { TryDismissBlockingWindows(); } if (_waitUntil > 0f) { if (Time.unscaledTime < _waitUntil) { return; } _waitUntil = -1f; _directorRunActive = false; } if (Queue.Count == 0) { return; } HarnessCommand cmd = Queue.Dequeue(); try { Execute(cmd); } catch (Exception ex) { _cmdFail++; Emit(cmd, ok: false, detail: "exception: " + ex.Message); WriteLastResult("error", "exception: " + ex.Message); } } private static void Execute(HarnessCommand cmd) { string action = (cmd.action ?? "").Trim().ToLowerInvariant(); if (string.IsNullOrEmpty(action)) { _cmdFail++; Emit(cmd, ok: false, detail: "missing action"); return; } switch (action) { case "wait_world": TryDismissBlockingWindows(); if (!WorldReady() || IsBlockingWindowOpen() || !HasAnyAliveUnit()) { RequeueFront(cmd); _waitUntil = Time.unscaledTime + 0.35f; if (!_readyLogged) { _readyLogged = true; LogService.LogInfo("[IdleSpectator][HARNESS] waiting for world load (will auto-close Welcome Back)"); } return; } _cmdOk++; Emit(cmd, ok: true, detail: "world_ready"); break; case "dismiss_windows": case "dismiss_ui": { bool closed = TryDismissBlockingWindows(forceAll: ParseBool(cmd.value, defaultValue: false)); _cmdOk++; Emit(cmd, ok: true, detail: closed ? "dismissed" : "none_open"); break; } case "wait": { float seconds = cmd.wait > 0f ? cmd.wait : ParseFloat(cmd.value, 1f); _directorRunActive = false; _waitUntil = Time.unscaledTime + Mathf.Max(0.05f, seconds); _cmdOk++; Emit(cmd, ok: true, detail: $"wait={seconds:0.##}s"); break; } case "director_run": { float seconds = cmd.wait > 0f ? cmd.wait : ParseFloat(cmd.value, 1f); seconds = Mathf.Max(0.1f, seconds); _directorRunActive = true; _waitUntil = Time.unscaledTime + seconds; _cmdOk++; Emit(cmd, ok: true, detail: $"director_run={seconds:0.##}s"); break; } case "fast_timing": { bool on = ParseBool(cmd.value, defaultValue: true); InterestDirector.SetHarnessFastTiming(on); SpeciesDiscovery.SetHarnessFastTiming(on); _cmdOk++; Emit(cmd, ok: true, detail: $"fast_timing={on}"); break; } case "force_live_feeds": { bool on = ParseBool(cmd.value, defaultValue: true); ForceLiveEventFeeds = on; _cmdOk++; Emit(cmd, ok: true, detail: $"force_live_feeds={on}"); break; } case "drop_clear": { InterestDropLog.Clear(); _cmdOk++; Emit(cmd, ok: true, detail: "drops_cleared"); break; } case "age_current": { float seconds = cmd.wait > 0f ? cmd.wait : ParseFloat(cmd.value, 2f); InterestDirector.HarnessAgeCurrent(seconds); _cmdOk++; Emit(cmd, ok: true, detail: $"aged={seconds:0.##}s score={InterestDirector.CurrentScoreLabel}"); break; } case "expire_grace": { InterestDirector.HarnessExpireInputGrace(); _cmdOk++; Emit(cmd, ok: true, detail: $"in_grace={InterestDirector.InInputGrace}"); break; } case "remember_tip": { _rememberedTip = InterestDirector.CurrentCandidate?.Label ?? CameraDirector.LastWatchLabel ?? ""; bool okTip = !string.IsNullOrEmpty(_rememberedTip); if (okTip) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, okTip, detail: $"tip='{_rememberedTip}'"); return; } case "remember_focus": { _rememberedFocusKey = FocusKey(); bool ok = !string.IsNullOrEmpty(_rememberedFocusKey) && _rememberedFocusKey != "-"; if (ok) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: $"focus={_rememberedFocusKey}"); break; } case "remember_related": { InterestCandidate scene = InterestDirector.CurrentCandidate; long id = scene != null ? (scene.PairPartnerId != 0 ? scene.PairPartnerId : EventFeedUtil.SafeId(scene.RelatedUnit)) : 0; _rememberedRelatedId = id; bool okRel = id != 0; if (okRel) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, okRel, detail: $"related={id}"); break; } case "spectator": { bool on = ParseBool(cmd.value, defaultValue: true); string expect = (cmd.expect ?? "").Trim().ToLowerInvariant(); // Manual idle toggle while Lore is open must not be undone by Lore close. if (ChronicleHud.Visible) { ChronicleHud.NotifyManualIdleToggle(); } SpectatorMode.SetActive(on); bool ok; string detail; if (expect == "blocked") { ok = on && !SpectatorMode.Active; detail = $"blocked idle={SpectatorMode.Active} enabled={ModSettings.Enabled}"; } else { ok = SpectatorMode.Active == on; detail = $"idle={SpectatorMode.Active}"; } if (ok) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail); break; } case "simulate_input": { string mode = (cmd.value ?? "drag").Trim().ToLowerInvariant(); string expect = (cmd.expect ?? "").Trim().ToLowerInvariant(); bool honorGrace = expect == "grace" || expect == "blocked" || mode == "grace"; // Only camera-drag is reliably injectable without OS input. bool exited = InterestDirector.SimulateManualInputExit(honorGrace: honorGrace); bool ok; string detail; if (honorGrace) { ok = !exited && SpectatorMode.Active; detail = $"grace_hold idle={SpectatorMode.Active} in_grace={InterestDirector.InInputGrace}"; } else { ok = exited && !SpectatorMode.Active; detail = $"exited idle={SpectatorMode.Active}"; } if (ok) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail); break; } case "discovery_reset": { SpeciesDiscovery.HarnessResetTracking(); _cmdOk++; Emit(cmd, ok: true, detail: "discovery_tracking_cleared"); break; } case "discovery_note": DoDiscoveryNote(cmd); break; case "discovery_mark": case "discovery_mark_presented": { string assetId = ResolveAssetId(cmd); SpeciesDiscovery.MarkPresented(assetId); _cmdOk++; Emit(cmd, ok: true, detail: $"presented={assetId} pending={SpeciesDiscovery.PendingCount}"); break; } case "discovery_drain": { bool drained = SpeciesDiscovery.HarnessForceDrainOnce(); if (drained) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, drained, detail: $"drained={drained} pending={SpeciesDiscovery.PendingCount} session={SpeciesDiscovery.DrainedThisSession}"); break; } case "watch_ghost": DoWatchGhost(cmd); break; case "chronicle_force": case "chronicle_note": { int n = Math.Max(1, cmd.count); string baseLabel = string.IsNullOrEmpty(cmd.label) ? "Killed" : cmd.label; int okN = 0; for (int i = 0; i < n; i++) { string label = n > 1 ? $"{baseLabel} #{i + 1}" : baseLabel; if (Chronicle.ForceNoteCurrentFocus(label)) { okN++; } } bool ok = okN == n; if (ok) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: $"forced={okN}/{n} history={Chronicle.HistoryCountFor(Chronicle.CurrentHistorySubjectId())} caption={WatchCaption.LastHeadline}"); break; } case "chronicle_milestone": { string kind = !string.IsNullOrEmpty(cmd.value) ? cmd.value : (!string.IsNullOrEmpty(cmd.label) ? cmd.label : "kill"); string other = cmd.expect ?? ""; bool ok = Chronicle.ForceLifeMilestoneOnFocus(kind, other); if (ok) { WatchCaption.ForceRefreshHistory(); _cmdOk++; } else { _cmdFail++; } long id = Chronicle.CurrentHistorySubjectId(); Emit(cmd, ok, detail: $"milestone={kind} history={Chronicle.HistoryCountFor(id)} activity={ActivityLog.CountFor(id)} otherRows={WatchCaption.CountHistoryOtherRows()}"); break; } case "chronicle_clear": { Chronicle.ClearSession(); _cmdOk++; Emit(cmd, ok: true, detail: "chronicle_cleared"); break; } case "activity_probe": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; string probe = ActivityInterestTable.DumpFocusProbe(focus); long id = 0; try { if (focus != null) { id = focus.getID(); } } catch { id = 0; } _cmdOk++; Emit(cmd, ok: true, detail: $"{probe} activity_count={ActivityLog.CountFor(id)}"); break; } case "activity_force": { long id = WatchCaption.CurrentUnitId; if (id == 0 && MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null) { try { id = MoveCamera._focus_unit.getID(); } catch { id = 0; } } string key = !string.IsNullOrEmpty(cmd.asset) ? cmd.asset : (!string.IsNullOrEmpty(cmd.value) ? cmd.value : "farm"); string line = !string.IsNullOrEmpty(cmd.label) ? cmd.label : key; bool asBeat = (cmd.tier ?? "").IndexOf("beat", System.StringComparison.OrdinalIgnoreCase) >= 0; string actorName = ""; try { if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null) { actorName = MoveCamera._focus_unit.getName() ?? ""; } } catch { actorName = ""; } string targetName = cmd.expect ?? ""; string speciesId = cmd.value ?? ""; string place = ""; string carrying = ""; // Optional: label "wheat@Riverhold" encodes carrying@place for unload tests. if (!string.IsNullOrEmpty(cmd.label) && cmd.label.IndexOf('@') >= 0 && (key.IndexOf("Unload", System.StringComparison.OrdinalIgnoreCase) >= 0 || key.IndexOf("Throw", System.StringComparison.OrdinalIgnoreCase) >= 0)) { string[] parts = cmd.label.Split(new[] { '@' }, 2); if (parts.Length == 2) { carrying = parts[0].Trim(); place = parts[1].Trim(); } } int n = Math.Max(1, cmd.count); int okN = 0; for (int i = 0; i < n; i++) { string one = n > 1 ? $"{line} #{i + 1}" : line; if (ActivityLog.ForceNote( id, key, one, asBeat, actorName, targetName, speciesId, place, carrying)) { okN++; } } WatchCaption.ForceRefreshHistory(); if (okN > 0) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, okN > 0, detail: $"forced={okN}/{n} key={key} activity={ActivityLog.CountFor(id)}"); break; } case "dossier_force_job": { string job = !string.IsNullOrEmpty(cmd.value) ? cmd.value : "farmer"; string reason = cmd.label ?? ""; bool ok = WatchCaption.ForceJobLabelOnFocus(job, reason); if (ok) { _cmdOk++; } else { _cmdFail++; } string shown = WatchCaption.Current?.ReasonLine ?? ""; Emit(cmd, ok, detail: $"job='{job}' reasonOverride='{reason}' row='{shown}'"); break; } case "dossier_force_headline": { string headline = !string.IsNullOrEmpty(cmd.value) ? cmd.value : (!string.IsNullOrEmpty(cmd.label) ? cmd.label : "Zezqzo Tikzkek Tak (scorpion)"); WatchCaption.ForceNametagHeadline(headline); _cmdOk++; Emit(cmd, true, detail: $"headline='{WatchCaption.LastHeadline}' shown='{WatchCaption.ShownNameChipText}'"); break; } case "dossier_force_named_reason": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; string name = ""; try { name = focus != null ? (focus.getName() ?? "") : ""; } catch { name = ""; } string clause = !string.IsNullOrEmpty(cmd.value) ? cmd.value : "is fighting"; if (string.IsNullOrEmpty(name)) { _cmdFail++; Emit(cmd, false, detail: "no_focus_name"); break; } // Optional related: label/expect name, else remembered happiness partner. string relatedName = !string.IsNullOrEmpty(cmd.expect) ? cmd.expect.Trim() : (!string.IsNullOrEmpty(cmd.label) ? cmd.label.Trim() : ""); long relatedId = 0; Actor relatedActor = null; if (string.Equals(relatedName, "auto", System.StringComparison.OrdinalIgnoreCase) || string.IsNullOrEmpty(relatedName)) { relatedActor = _happinessPartner != null && _happinessPartner.isAlive() ? _happinessPartner : null; if (relatedActor != null) { relatedId = EventFeedUtil.SafeId(relatedActor); relatedName = SafeName(relatedActor); } else { relatedName = ""; } } else { relatedId = ActivityLog.ResolveLivingUnitIdByName( relatedName, EventFeedUtil.SafeId(focus)); } string reason = name + " " + clause.Trim(); if (!string.IsNullOrEmpty(relatedName) && reason.IndexOf(relatedName, System.StringComparison.OrdinalIgnoreCase) < 0) { reason = reason + " " + relatedName; } bool ok = WatchCaption.ForceJobLabelOnFocus( !string.IsNullOrEmpty(cmd.asset) ? cmd.asset : "farmer", reason, relatedId, relatedName); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"reason='{WatchCaption.Current?.ReasonLine}' relatedId={relatedId} clickable={WatchCaption.ReasonOtherClickable}"); break; } case "dossier_click_reason_other": { bool ok = WatchCaption.ClickReasonOther(out long otherId); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"clicked={ok} otherId={otherId} lore={ChronicleHud.DetailUnitId}"); break; } case "dossier_click_history_other": { WatchCaption.ForceRefreshHistory(); bool ok = WatchCaption.ClickFirstHistoryOtherRow(out long otherId); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"clicked={ok} otherId={otherId} otherRows={WatchCaption.CountHistoryOtherRows()} lore={ChronicleHud.DetailUnitId}"); break; } case "dossier_clear_job": { WatchCaption.ClearHarnessJobOverrides(); if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null) { WatchCaption.SetFromActor(MoveCamera._focus_unit); } _cmdOk++; Emit(cmd, true, detail: "job overrides cleared"); break; } case "activity_clear": { ActivityLog.ClearSession(); WatchCaption.ForceRefreshHistory(); _cmdOk++; Emit(cmd, ok: true, detail: "activity_cleared"); break; } case "activity_age": { long id = WatchCaption.CurrentUnitId; if (id == 0 && MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null) { try { id = MoveCamera._focus_unit.getID(); } catch { id = 0; } } float seconds = 12f; if (!string.IsNullOrEmpty(cmd.value) && float.TryParse( cmd.value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float parsed) && parsed > 0f) { seconds = parsed; } int n = ActivityLog.HarnessAgeSubject(id, seconds); WatchCaption.ForceRefreshHistory(); _cmdOk++; Emit(cmd, ok: true, detail: $"aged={n} subject={id} seconds={seconds}"); break; } case "activity_sample_reset": { ActivityLog.ResetSampleCounters(); _cmdOk++; Emit(cmd, ok: true, detail: ActivityLog.FormatSampleStats()); break; } case "activity_stats": { string stats = ActivityLog.FormatSampleStats(); _cmdOk++; Emit(cmd, ok: true, detail: stats); break; } case "activity_status_reset": { long id = 0; try { if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null) { id = MoveCamera._focus_unit.getID(); } } catch { id = 0; } if (id == 0) { id = WatchCaption.CurrentUnitId; } ActivityLog.ResetStatusCountersForSubject(id); _cmdOk++; Emit( cmd, ok: true, detail: $"subject={id} gains={ActivityLog.StatusGainsSinceClear} " + $"losses={ActivityLog.StatusLossesSinceClear} refreshes={ActivityLog.StatusRefreshesSinceClear}"); break; } case "status_apply": { // Prefer explicit asset / related target so pair invincible does not only hit Follow. Actor focus = null; if (!string.IsNullOrEmpty(cmd.asset) && cmd.asset != "auto") { focus = ResolveUnit(cmd.asset); } if (focus == null) { focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; } string statusId = !string.IsNullOrEmpty(cmd.value) ? cmd.value.Trim() : "burning"; float timer = 0f; if (!string.IsNullOrEmpty(cmd.label) && float.TryParse(cmd.label, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsed)) { timer = parsed; } bool expectBlocked = (cmd.expect ?? "").IndexOf("block", StringComparison.OrdinalIgnoreCase) >= 0; bool optional = (cmd.expect ?? "").IndexOf("optional", StringComparison.OrdinalIgnoreCase) >= 0; bool alive = false; try { alive = focus != null && focus.isAlive(); } catch { alive = false; } bool applied = alive && StatusGameApi.TryAddStatus(focus, statusId, timer); // Some units briefly refuse a status (immunity / race); one retry covers that class. if (!applied && alive && !expectBlocked) { applied = StatusGameApi.TryAddStatus(focus, statusId, timer); } bool ok = expectBlocked ? !applied : applied; if (optional) { // Seed-only second statuses: dossier only needs one chip for the assert. ok = true; } if (ok) { _cmdOk++; } else { _cmdFail++; } WatchCaption.ForceRefreshHistory(); Emit( cmd, ok, detail: $"status={statusId} applied={applied} expectBlocked={expectBlocked} optional={optional} alive={alive} " + $"gains={ActivityLog.StatusGainsSinceClear} refreshes={ActivityLog.StatusRefreshesSinceClear} " + $"active={StatusGameApi.HasStatus(focus, statusId)}"); break; } case "status_apply_nearby": { // value = status id; count = how many nearby living units to afflict (default 6). Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; string statusId = !string.IsNullOrEmpty(cmd.value) ? cmd.value.Trim() : "burning"; float timer = 30f; if (!string.IsNullOrEmpty(cmd.label) && float.TryParse(cmd.label, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsedTimer)) { timer = parsedTimer; } int want = Math.Max(1, cmd.count > 0 ? cmd.count : 6); int applied = 0; if (focus != null && focus.isAlive() && StatusGameApi.TryAddStatus(focus, statusId, timer)) { applied++; } foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic()) { if (applied >= want) { break; } if (actor == null || actor == focus || !actor.isAlive()) { continue; } if (StatusGameApi.TryAddStatus(actor, statusId, timer)) { applied++; } } bool ok = applied >= Math.Min(2, want); if (ok) { _cmdOk++; } else { _cmdFail++; } WatchCaption.ForceRefreshHistory(); Emit(cmd, ok, detail: $"status={statusId} applied={applied}/{want}"); break; } case "status_remove": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; string statusId = !string.IsNullOrEmpty(cmd.value) ? cmd.value.Trim() : "burning"; bool ok = focus != null && StatusGameApi.TryFinishStatus(focus, statusId); if (ok) { _cmdOk++; } else { _cmdFail++; } WatchCaption.ForceRefreshHistory(); Emit( cmd, ok, detail: $"status={statusId} finished={ok} losses={ActivityLog.StatusLossesSinceClear} " + $"active={StatusGameApi.HasStatus(focus, statusId)}"); break; } case "hatch_emit": { Actor focus = ResolveUnit( string.IsNullOrEmpty(cmd.asset) || cmd.asset == "auto" ? null : cmd.asset); if (focus == null) { focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; } bool alive = false; try { alive = focus != null && focus.isAlive(); } catch { alive = false; } if (alive) { InterestFeeds.EmitHatch(focus, "harness"); _cmdOk++; } else { _cmdFail++; } Emit(cmd, alive, detail: alive ? EventReason.Hatch(focus) : "no unit"); break; } case "decision_emit": { // Live DeferredInterestFeed path (same as setDecisionCooldown patch). Actor focus = ResolveUnit( string.IsNullOrEmpty(cmd.asset) || cmd.asset == "auto" ? null : cmd.asset); if (focus == null) { focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; } string decisionId = (cmd.expect ?? cmd.value ?? "sexual_reproduction_try").Trim(); bool alive = false; try { alive = focus != null && focus.isAlive(); } catch { alive = false; } if (!alive || string.IsNullOrEmpty(decisionId)) { _cmdFail++; Emit(cmd, ok: false, detail: "no unit or decision id"); break; } bool prevForce = ForceLiveEventFeeds; ForceLiveEventFeeds = true; try { DeferredInterestFeed.EmitDecision(decisionId, focus); _cmdOk++; Emit( cmd, ok: true, detail: $"emitted decision={decisionId} unit={SafeName(focus)}"); } finally { ForceLiveEventFeeds = prevForce; } break; } case "status_clear_all": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; bool ok = focus != null && StatusGameApi.TryFinishAll(focus); if (ok) { _cmdOk++; } else { _cmdFail++; } WatchCaption.ForceRefreshHistory(); Emit( cmd, ok, detail: $"cleared={ok} losses={ActivityLog.StatusLossesSinceClear} " + $"active_count={StatusGameApi.CountActiveStatuses(focus)}"); break; } case "status_force_note": { long id = WatchCaption.CurrentUnitId; Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; if (id == 0 && focus != null) { try { id = focus.getID(); } catch { id = 0; } } string statusId = !string.IsNullOrEmpty(cmd.value) ? cmd.value.Trim() : "zzz_harness_fake_status"; bool gained = (cmd.expect ?? "gain").IndexOf("loss", StringComparison.OrdinalIgnoreCase) < 0; string actorName = ""; try { if (focus != null) { actorName = focus.getName() ?? ""; } } catch { actorName = ""; } bool ok = ActivityLog.ForceStatusNote(id, statusId, gained, actorName); WatchCaption.ForceRefreshHistory(); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: $"forced status={statusId} gained={gained} id={id}"); break; } case "happiness_reset": { long id = 0; try { if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null) { id = MoveCamera._focus_unit.getID(); } } catch { id = 0; } if (id == 0) { id = WatchCaption.CurrentUnitId; } HappinessEventRouter.ResetCountersForSubject(id); ActivityLog.ResetSampleCounters(); _cmdOk++; Emit( cmd, ok: true, detail: $"subject={id} notes={HappinessEventRouter.NotesSinceClear} " + $"ring={HappinessEventRouter.RingWritesSinceClear}"); break; } case "happiness_apply": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; string effectId = !string.IsNullOrEmpty(cmd.value) ? cmd.value.Trim() : "just_ate"; int bonus = 0; if (!string.IsNullOrEmpty(cmd.label) && int.TryParse(cmd.label, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsedBonus)) { bonus = parsedBonus; } bool expectBlocked = (cmd.expect ?? "").IndexOf("block", StringComparison.OrdinalIgnoreCase) >= 0; bool alive = false; try { alive = focus != null && focus.isAlive(); } catch { alive = false; } bool applied = alive && HappinessGameApi.TryChangeHappiness(focus, effectId, bonus); bool ok = expectBlocked ? !applied : applied; if (ok) { _cmdOk++; } else { _cmdFail++; } WatchCaption.ForceRefreshHistory(); Emit( cmd, ok, detail: $"effect={effectId} applied={applied} expectBlocked={expectBlocked} " + $"notes={HappinessEventRouter.NotesSinceClear} ring={HappinessEventRouter.RingWritesSinceClear} " + $"last={HappinessEventRouter.LastEffectId}"); break; } case "happiness_force_note": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; long id = 0; try { if (focus != null) { id = focus.getID(); } } catch { id = 0; } // Prefer live focus over a stale dossier CurrentUnitId. if (id == 0) { id = WatchCaption.CurrentUnitId; } string effectId = !string.IsNullOrEmpty(cmd.value) ? cmd.value.Trim() : "just_ate"; string related = cmd.label ?? ""; string actorName = ""; try { if (focus != null) { actorName = focus.getName() ?? ""; } } catch { actorName = ""; } bool ok = ActivityLog.ForceHappinessNote(id, effectId, actorName, related); Actor registerUnit = focus; if (registerUnit == null && InterestDirector.CurrentCandidate != null) { registerUnit = InterestDirector.CurrentCandidate.FollowUnit; } if (registerUnit == null && id != 0) { foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic()) { try { if (actor != null && actor.getID() == id) { registerUnit = actor; break; } } catch { // continue } } } if (registerUnit == null && MoveCamera.hasFocusUnit()) { registerUnit = MoveCamera._focus_unit; } if (ok && registerUnit != null) { string label = string.IsNullOrEmpty(related) ? (actorName + " · " + effectId) : (actorName + " mourns " + related); InterestCandidate registered = InterestFeeds.RegisterHappinessSignal( registerUnit, effectId, label, related); if (registered == null) { ok = false; } } else if (ok) { ok = false; } WatchCaption.ForceRefreshHistory(); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"forced happiness={effectId} related={related} id={id} unit={(registerUnit != null)} pending={InterestRegistry.PendingCount} count={InterestRegistry.Count}"); break; } case "happiness_inject_all": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; bool alive = false; try { alive = focus != null && focus.isAlive(); } catch { alive = false; } if (!alive) { _cmdFail++; Emit(cmd, ok: false, detail: "no living focus"); break; } List ids = ActivityAssetCatalog.EnumerateLiveHappinessIds(); int applied = 0; int blocked = 0; for (int i = 0; i < ids.Count; i++) { if (HappinessGameApi.TryChangeHappiness(focus, ids[i], 0)) { applied++; } else { blocked++; } } WatchCaption.ForceRefreshHistory(); bool ok = ids.Count > 0 && applied + blocked == ids.Count; if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"live={ids.Count} applied={applied} blocked={blocked} " + $"notes={HappinessEventRouter.NotesSinceClear}"); break; } case "happiness_remember_partner": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : _lastSpawned; _happinessPartner = focus; bool ok = _happinessPartner != null; if (ok) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: ok ? $"partner={SafeName(_happinessPartner)}" : "no partner"); break; } case "focus_partner": { Actor partner = _happinessPartner; bool ok = partner != null && partner.isAlive(); if (ok) { CameraDirector.FocusUnit(partner); _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: ok ? $"focusPartner={SafeName(partner)}" : "no partner"); break; } case "happiness_bond_lovers": { Actor a = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; Actor b = _happinessPartner; bool ok = false; try { if (a != null && b != null && a.isAlive() && b.isAlive() && a != b) { a.becomeLoversWith(b); ok = true; } } catch { ok = false; } if (ok) { _cmdOk++; } else { _cmdFail++; } WatchCaption.ForceRefreshHistory(); Emit( cmd, ok, detail: $"a={SafeName(a)} b={SafeName(b)} notes={HappinessEventRouter.NotesSinceClear}"); break; } case "relationship_set_parent": { Actor child = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : _lastSpawned; Actor parent = _happinessPartner; bool ok = false; string detail; ForceRelationshipFeeds = true; try { if (child != null && parent != null && child.isAlive() && parent.isAlive() && child != parent) { child.setParent1(parent); ok = true; detail = $"parent={SafeName(parent)} child={SafeName(child)}"; } else { detail = "need focus child + remembered parent"; } } catch (Exception ex) { detail = "exception: " + ex.Message; ok = false; } finally { ForceRelationshipFeeds = false; } if (ok) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: detail); break; } case "happiness_bond_friends": { Actor a = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; Actor b = _happinessPartner; bool ok = false; try { if (a != null && b != null && a.isAlive() && b.isAlive() && a != b) { a.setBestFriend(b, true); b.setBestFriend(a, true); ok = true; } } catch { ok = false; } if (ok) { _cmdOk++; } else { _cmdFail++; } WatchCaption.ForceRefreshHistory(); Emit( cmd, ok, detail: $"a={SafeName(a)} b={SafeName(b)} notes={HappinessEventRouter.NotesSinceClear}"); break; } case "happiness_kill_partner": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; Actor partner = _happinessPartner; string err = ""; try { if (partner != null && partner.isAlive()) { partner.die(true, AttackType.Divine); } } catch (Exception ex) { err = ex.Message ?? "die_exception"; } bool ok = false; try { ok = partner != null && !partner.isAlive(); } catch { ok = false; } // Keep the surviving focus so grief asserts resolve the right subject. try { if (focus != null && focus.isAlive()) { CameraDirector.FocusUnit(focus); _activitySubjectId = focus.getID(); } } catch { // ignore } if (ok) { _cmdOk++; } else { _cmdFail++; } WatchCaption.ForceRefreshHistory(); Emit( cmd, ok, detail: $"killed={SafeName(partner)} ok={ok}" + (string.IsNullOrEmpty(err) ? "" : " err=" + err)); break; } case "happiness_burst": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; string effectId = !string.IsNullOrEmpty(cmd.value) ? cmd.value.Trim() : "just_started_war"; int n = Math.Max(1, cmd.count > 0 ? cmd.count : 64); bool alive = false; try { alive = focus != null && focus.isAlive(); } catch { alive = false; } if (!alive) { _cmdFail++; Emit(cmd, ok: false, detail: "no living focus"); break; } float t0 = Time.realtimeSinceStartup; int applied = 0; for (int i = 0; i < n; i++) { if (HappinessGameApi.TryChangeHappiness(focus, effectId, 0)) { applied++; } } float ms = (Time.realtimeSinceStartup - t0) * 1000f; HappinessEventRouter.FlushAggregatesNow(); bool ok = applied > 0 && ms < 250f; if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"effect={effectId} n={n} applied={applied} ms={ms:0.0} " + $"agg={HappinessEventRouter.AggregateSummariesSinceClear}"); break; } case "kill_focus": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; bool ok = false; try { if (focus != null && focus.isAlive()) { focus.die(true, AttackType.Age); ok = !focus.isAlive(); } } catch { ok = false; } if (ok) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: $"killed={ok}"); break; } case "lore_detail_pane": { string raw = (cmd.value ?? cmd.label ?? "life").Trim().ToLowerInvariant(); ChronicleHud.DetailPane pane = raw.StartsWith("act") ? ChronicleHud.DetailPane.Activity : ChronicleHud.DetailPane.Life; ChronicleHud.SetDetailPane(pane); bool ok = ChronicleHud.DetailUnitId != 0 && ChronicleHud.ActiveDetailPane == pane; if (ok) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: $"pane={ChronicleHud.ActiveDetailPane} detailId={ChronicleHud.DetailUnitId} " + $"actRows={ChronicleHud.LastDetailActivityRows} lifeRows={ChronicleHud.LastDetailHistoryRows}"); break; } case "chronicle_world_force": { string label = !string.IsNullOrEmpty(cmd.label) ? cmd.label : (!string.IsNullOrEmpty(cmd.value) ? cmd.value : "War: harness vs test"); bool ok = Chronicle.ForceWorldNote(label); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: $"world_count={Chronicle.WorldCount} label={label}"); break; } case "chronicle_death_sample": { string typeName = !string.IsNullOrEmpty(cmd.value) ? cmd.value : "Age"; bool ok = Chronicle.ForceDeathSample(typeName); if (ok) { _cmdOk++; } else { _cmdFail++; } ChronicleEntry latest = Chronicle.Latest; Emit(cmd, ok, detail: $"type={typeName} latest={(latest != null ? latest.Line : "")}"); break; } case "chronicle_tab": case "lore_tab": case "world_memory_open": { Chronicle.ShowHud(); string tabRaw = (cmd.value ?? cmd.label ?? "world").Trim().ToLowerInvariant(); if (tabRaw == "fallen" || tabRaw == "dead") { ChronicleHud.SetTab(ChronicleHud.LoreTab.Fallen); } else if (tabRaw == "living" || tabRaw == "characters" || tabRaw == "character" || tabRaw == "chars" || tabRaw == "history") { ChronicleHud.SetTab(ChronicleHud.LoreTab.Living); } else { ChronicleHud.SetTab(ChronicleHud.LoreTab.World); } bool open = Chronicle.HudVisible; if (open) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, open, detail: $"hud={open} tab={ChronicleHud.ActiveTab} memory={Chronicle.MemoryCount} recent={Chronicle.RecentFocusCount}"); break; } case "lore_refresh": case "chronicle_refresh": { ChronicleHud.RefreshCastList(); _cmdOk++; Emit( cmd, ok: true, detail: $"stale={ChronicleHud.CastListStale} top='{ChronicleHud.LastListTopTitle}' tab={ChronicleHud.ActiveTab}"); break; } case "lore_search": { Chronicle.ShowHud(); ChronicleHud.SetTab(ChronicleHud.LoreTab.Living); ChronicleHud.SetSearch(cmd.value ?? cmd.label ?? ""); _cmdOk++; Emit(cmd, ok: true, detail: $"search='{ChronicleHud.SearchText}'"); break; } case "lore_favorites": { Chronicle.ShowHud(); string v = (cmd.value ?? "true").Trim().ToLowerInvariant(); bool on = v != "false" && v != "0" && v != "off"; ChronicleHud.SetFavoritesOnly(on); // Filter is a list view - leave detail if we were in one. if (ChronicleHud.DetailUnitId != 0) { ChronicleHud.ClearUnitDetail(); } _cmdOk++; Emit( cmd, ok: true, detail: $"favoritesOnly={ChronicleHud.FavoritesOnly} tab={ChronicleHud.ActiveTab}"); break; } case "lore_pick": { Chronicle.ShowHud(); ChronicleHud.SetTab(ChronicleHud.LoreTab.Living); string needle = cmd.value ?? cmd.label ?? ""; if (needle == "auto") { needle = ""; } bool ok = ChronicleHud.PickFirstCharacter(needle); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: $"picked={ok} focus={MoveCamera.hasFocusUnit()} dossier={WatchCaption.LastHeadline} recent={Chronicle.RecentFocusCount}"); break; } case "lore_open_focus": case "chronicle_open_focus": { ChronicleHud.OpenSyncedFromFocus(); bool open = Chronicle.HudVisible; if (open) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, open, detail: $"lore={Chronicle.HudVisible} detail={ChronicleHud.DetailUnitId} dossierId={WatchCaption.CurrentUnitId} idle={SpectatorMode.Active} follow={ChronicleHud.FollowFocus}"); break; } case "lore_pick_fallen": { Chronicle.ShowHud(); ChronicleHud.SetTab(ChronicleHud.LoreTab.Fallen); string needle = cmd.value ?? cmd.label ?? ""; if (needle == "auto") { needle = ""; } bool ok = ChronicleHud.PickFirstFallen(needle); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: $"picked={ok} detail={ChronicleHud.DetailUnitId} follow={ChronicleHud.FollowFocus} hist={Chronicle.HistoryCountFor(ChronicleHud.DetailUnitId)} fallenFocus={Chronicle.LastFallenFocusDetail} manner={Chronicle.LatestDeathMannerFor(ChronicleHud.DetailUnitId)}"); break; } case "chronicle_orphan": { int n = Math.Max(1, cmd.count); string name = !string.IsNullOrEmpty(cmd.label) ? cmd.label : "Orphan"; string species = !string.IsNullOrEmpty(cmd.asset) ? cmd.asset : "creature"; string prefix = !string.IsNullOrEmpty(cmd.value) ? cmd.value : "Fallen"; AttackType attackType = AttackType.Age; if (!string.IsNullOrEmpty(cmd.expect) && System.Enum.TryParse(cmd.expect, ignoreCase: true, out AttackType parsedAttack)) { attackType = parsedAttack; } long killerId = 0; Vector3 deathPos = Vector3.zero; string mode = (cmd.tier ?? "").Trim().ToLowerInvariant(); bool favorite = mode.IndexOf("fav", System.StringComparison.Ordinal) >= 0; Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; if (mode.IndexOf("killer", System.StringComparison.Ordinal) >= 0 && focus != null && focus.isAlive()) { killerId = focus.getID(); // Offset so place/skull focus is distinct from following the killer. Vector2 killerPos = focus.current_position; deathPos = new Vector3(killerPos.x + 2.5f, killerPos.y, 0f); } else if (mode.IndexOf("place", System.StringComparison.Ordinal) >= 0 || favorite || string.IsNullOrEmpty(mode)) { if (mode.IndexOf("place", System.StringComparison.Ordinal) >= 0 || favorite) { if (focus != null) { deathPos = focus.current_position; } else if (MoveCamera.instance != null) { deathPos = MoveCamera.instance.transform.position; deathPos.z = 0f; } if (deathPos == Vector3.zero) { deathPos = new Vector3(48f, 48f, 0f); } } } long id = Chronicle.ForceOrphanHistory( name, species, n, prefix, attackType, killerId, deathPos, favorite); DeathManner manner = Chronicle.LatestDeathMannerFor(id); bool ok = id != 0 && Chronicle.HistoryCountFor(id) >= n && (!favorite || Chronicle.IsFavoriteSubject(id)); if (ok && deathPos.sqrMagnitude > 0.0001f) { int tx = Mathf.FloorToInt(deathPos.x); int ty = Mathf.FloorToInt(deathPos.y); GraveMarkers.ForceAdd( id, name, tx, ty, deathPos, Chronicle.FormatGraveDeathSubtitle(id), favorite); } if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"orphanId={id} hist={Chronicle.HistoryCountFor(id)} want={n} manner={manner} killer={killerId} mode={mode} fav={Chronicle.IsFavoriteSubject(id)} stacks={GraveMarkers.StackCount}"); break; } case "grave_clear": { GraveMarkers.Clear(); _cmdOk++; Emit(cmd, ok: true, detail: "graves_cleared"); break; } case "grave_force": { // value=name or "A,B,C" (same tile), count=entries, tier="dx,dy" offset from focus/camera. // Comma names / count>1 share one captured tile so a walking focus cannot split the stack. string rawName = !string.IsNullOrEmpty(cmd.value) ? cmd.value : "GraveDummy"; string[] named = rawName.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < named.Length; i++) { named[i] = named[i].Trim(); } int n = named.Length > 1 ? named.Length : Math.Max(1, cmd.count); Vector3 pos = Vector3.zero; if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null) { pos = MoveCamera._focus_unit.current_position; } else if (MoveCamera.instance != null) { pos = MoveCamera.instance.transform.position; pos.z = 0f; } if (pos.sqrMagnitude < 0.0001f) { pos = new Vector3(48f, 48f, 0f); } int tx = Mathf.FloorToInt(pos.x); int ty = Mathf.FloorToInt(pos.y); if (!string.IsNullOrEmpty(cmd.tier) && cmd.tier.Contains(",")) { string[] parts = cmd.tier.Split(','); if (parts.Length >= 2 && int.TryParse(parts[0].Trim(), out int ox) && int.TryParse(parts[1].Trim(), out int oy)) { tx += ox; ty += oy; pos = new Vector3(tx + 0.5f, ty + 0.5f, 0f); } } int before = GraveMarkers.EntryCountAt(tx, ty); for (int i = 0; i < n; i++) { string nm = named.Length > 1 ? named[i] : (n > 1 ? rawName + i : rawName); long id = Chronicle.ForceOrphanHistory( nm, !string.IsNullOrEmpty(cmd.asset) ? cmd.asset : "human", 1, "Grave", AttackType.Age, 0, pos, favorite: false); GraveMarkers.ForceAdd( id, nm, tx, ty, pos, Chronicle.FormatGraveDeathSubtitle(id), favorite: false); } int after = GraveMarkers.EntryCountAt(tx, ty); bool gOk = after >= before + n && GraveMarkers.StackCount >= 1; if (gOk) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, gOk, detail: $"tile={tx},{ty} entries={after} stacks={GraveMarkers.StackCount} added={n}"); break; } case "grave_open": { Vector3 pos = Vector3.zero; if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null) { pos = MoveCamera._focus_unit.current_position; } else if (MoveCamera.instance != null) { pos = MoveCamera.instance.transform.position; pos.z = 0f; } int tx = Mathf.FloorToInt(pos.x); int ty = Mathf.FloorToInt(pos.y); if (!string.IsNullOrEmpty(cmd.value) && cmd.value.Contains(",")) { string[] parts = cmd.value.Split(','); if (parts.Length >= 2 && int.TryParse(parts[0].Trim(), out int px) && int.TryParse(parts[1].Trim(), out int py)) { tx = px; ty = py; } } bool opened = ChronicleHud.OpenGraveStack(tx, ty, pauseIdle: true); if (opened) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, opened, detail: $"tile={tx},{ty} open={opened} scope={ChronicleHud.GraveStackScopeActive} detail={ChronicleHud.DetailUnitId} stacks={GraveMarkers.StackCount}"); break; } case "grave_search": { ChronicleHud.SetSearch(cmd.value ?? ""); _cmdOk++; Emit( cmd, ok: true, detail: $"query='{ChronicleHud.SearchText}' filtered={ChronicleHud.GraveStackFilteredCount} scope={ChronicleHud.GraveStackScopeActive}"); break; } case "grave_open_lore": { // value=name: search any stack. Else focus/camera tile, then current Lore grave scope. string needle = (cmd.value ?? "").Trim(); int tx = 0; int ty = 0; long unitId = 0; GraveMarkers.GraveStack stack = null; GraveMarkers.GraveEntry pick = null; if (!string.IsNullOrEmpty(needle) && GraveMarkers.TryFindEntryByName(needle, out pick, out stack)) { tx = stack.TileX; ty = stack.TileY; unitId = pick.UnitId; } else { Vector3 pos = Vector3.zero; if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null) { pos = MoveCamera._focus_unit.current_position; } else if (MoveCamera.instance != null) { pos = MoveCamera.instance.transform.position; pos.z = 0f; } tx = Mathf.FloorToInt(pos.x); ty = Mathf.FloorToInt(pos.y); if (!GraveMarkers.TryGetStack(tx, ty, out stack) && ChronicleHud.GraveStackScopeActive) { tx = ChronicleHud.GraveStackTileX; ty = ChronicleHud.GraveStackTileY; GraveMarkers.TryGetStack(tx, ty, out stack); } if (stack != null && stack.Entries.Count > 0) { pick = stack.Entries[stack.Entries.Count - 1]; unitId = pick.UnitId; } } bool loreOk = false; if (unitId != 0 && stack != null) { InspectUi.MarkGraveSession(); ChronicleHud.OpenGraveStack(tx, ty, pauseIdle: true); if (ChronicleHud.DetailUnitId != unitId) { ChronicleHud.OpenUnitHistory( unitId, pauseIdle: true, followFocus: false, preferPlace: true); } loreOk = ChronicleHud.DetailUnitId == unitId && Chronicle.LastFallenFocusDetail == "place"; } if (loreOk) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, loreOk, detail: $"tile={tx},{ty} unit={unitId} detail={ChronicleHud.DetailUnitId} fallenFocus={Chronicle.LastFallenFocusDetail} scope={ChronicleHud.GraveStackScopeActive}"); break; } case "grave_set_permanent": { // value=true|false; optional tier="tx,ty" else Lore grave scope / focus tile. int tx = ChronicleHud.GraveStackTileX; int ty = ChronicleHud.GraveStackTileY; if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null) { Vector3 p = MoveCamera._focus_unit.current_position; int ftx = Mathf.FloorToInt(p.x); int fty = Mathf.FloorToInt(p.y); if (GraveMarkers.TryGetStack(ftx, fty, out _)) { tx = ftx; ty = fty; } } if (!ChronicleHud.GraveStackScopeActive && !GraveMarkers.TryGetStack(tx, ty, out _) && MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null) { Vector3 p = MoveCamera._focus_unit.current_position; tx = Mathf.FloorToInt(p.x); ty = Mathf.FloorToInt(p.y); } if (!string.IsNullOrEmpty(cmd.tier) && cmd.tier.Contains(",")) { string[] parts = cmd.tier.Split(','); if (parts.Length >= 2 && int.TryParse(parts[0].Trim(), out int px) && int.TryParse(parts[1].Trim(), out int py)) { tx = px; ty = py; } } bool want = ParseBool( !string.IsNullOrEmpty(cmd.value) ? cmd.value : cmd.expect, defaultValue: true); bool setOk = GraveMarkers.TrySetStackPermanent(tx, ty, want); if (setOk && ChronicleHud.GraveStackScopeActive) { // Refresh Keep chrome. ChronicleHud.OpenGraveStack(tx, ty, pauseIdle: false); } if (setOk) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, setOk, detail: $"tile={tx},{ty} permanent={GraveMarkers.IsStackPermanent(tx, ty)} want={want}"); break; } case "grave_delete": { int before = GraveMarkers.StackCount; bool delOk = ChronicleHud.DeleteActiveGraveStack(); bool gone = delOk && !ChronicleHud.GraveStackScopeActive && GraveMarkers.StackCount < before; if (gone) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, gone, detail: $"deleted={delOk} stacks={GraveMarkers.StackCount} before={before} scope={ChronicleHud.GraveStackScopeActive} lore={Chronicle.HudVisible}"); break; } case "inspect_dismiss": { InspectUi.DismissAll(); bool cleared = !SpectatorMode.Active && !Chronicle.HudVisible && !WatchCaption.Visible && !ChronicleHud.GraveStackScopeActive && !InspectUi.GraveSessionActive; if (cleared) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, cleared, detail: $"cleared={cleared} idle={SpectatorMode.Active} lore={Chronicle.HudVisible} dossier={WatchCaption.Visible} scope={ChronicleHud.GraveStackScopeActive} graveSession={InspectUi.GraveSessionActive}"); break; } case "lore_focus_killer": { bool kOk = ChronicleHud.FocusDetailKiller(); if (kOk) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, kOk, detail: $"ok={kOk} fallenFocus={Chronicle.LastFallenFocusDetail} detail={ChronicleHud.DetailUnitId}"); break; } case "chronicle_orphan_flood": { // Stress subject-cap pruning (does not need living units). int n = Math.Max(1, cmd.count); int made = 0; for (int i = 0; i < n; i++) { long id = Chronicle.ForceOrphanHistory( "Flood" + i, "human", 1, "Flood", AttackType.Age); if (id != 0) { made++; } } bool ok = made == n && Chronicle.HistorySubjectCount <= Chronicle.MaxHistorySubjects; if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"made={made}/{n} subjects={Chronicle.HistorySubjectCount} max={Chronicle.MaxHistorySubjects} dossiers={Chronicle.FinalDossierCount}"); break; } case "chronicle_set_subject_cap": { int cap = Math.Max(40, cmd.count); if (!string.IsNullOrEmpty(cmd.value) && int.TryParse(cmd.value.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed)) { cap = parsed; } Chronicle.SetMaxHistorySubjects(cap); bool ok = Chronicle.MaxHistorySubjects == Mathf.Clamp(cap, 40, Chronicle.AbsoluteMaxHistorySubjects) && Chronicle.HistorySubjectCount <= Chronicle.MaxHistorySubjects; if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"cap={Chronicle.MaxHistorySubjects} subjects={Chronicle.HistorySubjectCount}"); break; } case "chronicle_subject_bench": { // value: comma-separated sizes, e.g. "320,640,1280,2560" // count: events per subject (default 3). expect: max avg fallen ms budget (default 2). string sizesRaw = string.IsNullOrEmpty(cmd.value) ? "320,640,1280,2560,4096,8192" : cmd.value; int eventsPer = Math.Max(1, cmd.count); if (eventsPer == 1 && string.IsNullOrEmpty(cmd.label)) { eventsPer = 3; } float budgetMs = 2f; if (!string.IsNullOrEmpty(cmd.expect) && float.TryParse(cmd.expect, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsedBudget)) { budgetMs = parsedBudget; } string[] parts = sizesRaw.Split(','); var reports = new List(); int best = 0; float bestAvg = -1f; bool any = false; for (int i = 0; i < parts.Length; i++) { string p = parts[i].Trim(); if (!int.TryParse(p, NumberStyles.Integer, CultureInfo.InvariantCulture, out int size) || size < 40) { continue; } string report = Chronicle.BenchSubjectLoad(size, eventsPer, iters: 25); reports.Add(report); any = true; float avg = float.MaxValue; const string needle = "fallenAvgMs="; int idx = report.IndexOf(needle, StringComparison.Ordinal); if (idx >= 0) { int start = idx + needle.Length; int end = start; while (end < report.Length && (char.IsDigit(report[end]) || report[end] == '.')) { end++; } float.TryParse( report.Substring(start, end - start), NumberStyles.Float, CultureInfo.InvariantCulture, out avg); } // Highest size that stays within the ms budget. if (avg <= budgetMs && size > best) { best = size; bestAvg = avg; } } if (best <= 0) { best = Chronicle.DefaultMaxHistorySubjects; bestAvg = budgetMs; } Chronicle.SetMaxHistorySubjects(best); Chronicle.ClearSession(); bool ok = any && reports.Count > 0; if (ok) { _cmdOk++; } else { _cmdFail++; } string joined = string.Join(" || ", reports); Emit( cmd, ok, detail: $"budgetMs={budgetMs:0.00} recommend={best} recommendAvgMs={bestAvg:0.00} capNow={Chronicle.MaxHistorySubjects} | {joined}"); break; } case "lore_close": case "chronicle_close": { Chronicle.HideHud(); _cmdOk++; Emit(cmd, ok: true, detail: $"lore={Chronicle.HudVisible}"); break; } case "chronicle_open": { Chronicle.ShowHud(); ChronicleHud.SetTab(ChronicleHud.LoreTab.World); bool open = Chronicle.HudVisible; if (open) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, open, detail: $"hud={open} memory={Chronicle.MemoryCount} age={Chronicle.CurrentAgeName}"); break; } case "dossier_favorite": { Actor target = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; string v = (cmd.value ?? "").Trim().ToLowerInvariant(); if (v == "true" || v == "1" || v == "on" || v == "false" || v == "0" || v == "off") { bool want = v == "true" || v == "1" || v == "on"; bool ok = false; try { if (target != null && target.isAlive()) { if (target.isFavorite() != want) { target.switchFavorite(); } ok = target.isFavorite() == want; WatchCaption.SetFromActor(target); } } catch { ok = false; } if (ok) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: $"favorite={want} set"); break; } WatchCaption.ToggleFavorite(); bool fav = false; try { fav = target != null && target.isFavorite(); } catch { // ignore } _cmdOk++; Emit(cmd, ok: true, detail: $"favorite={fav}"); break; } case "dossier_expand": case "dossier_history_open": { WatchCaption.OpenFullHistoryInLore(); bool open = Chronicle.HudVisible && ChronicleHud.DetailUnitId != 0; if (open) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, open, detail: $"lore={Chronicle.HudVisible} detail={ChronicleHud.DetailUnitId} idle={SpectatorMode.Active} shown={WatchCaption.LastHistoryShown}"); break; } case "lore_back": { ChronicleHud.ClearUnitDetail(); _cmdOk++; Emit(cmd, ok: true, detail: $"detail={ChronicleHud.DetailUnitId} tab={ChronicleHud.ActiveTab}"); break; } case "chronicle_jump": { bool ok = Chronicle.JumpToLatest(); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"jumped={ok} detail={Chronicle.LastJumpDetail} focus={MoveCamera.hasFocusUnit()} pos={Chronicle.LastJumpPosition}"); break; } case "lore_jump_world": { Chronicle.ShowHud(); ChronicleHud.SetTab(ChronicleHud.LoreTab.World); ChronicleEntry target = null; IReadOnlyList snap = Chronicle.SnapshotMemory(); for (int i = snap.Count - 1; i >= 0; i--) { ChronicleEntry e = snap[i]; if (e == null || e.Kind == ChronicleKind.AgeChapter) { continue; } if (e.Kind == ChronicleKind.World || e.IsLegend) { target = e; break; } } bool ok = target != null && Chronicle.JumpTo(target); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"jumped={ok} detail={Chronicle.LastJumpDetail} focus={MoveCamera.hasFocusUnit()} line={(target != null ? target.Line : "")}"); break; } case "screenshot": { bool ok = CaptureHarnessScreenshot(cmd.value); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: $"path={LastScreenshotPath} layoutOk={WatchCaption.LastLayoutOk} size={WatchCaption.LastPanelSize}"); break; } case "spawn": DoSpawn(cmd); break; case "pick_unit": DoPickUnit(cmd); break; case "focus": DoFocus(cmd); break; case "watch": DoWatch(cmd); break; case "trigger_interest": DoTriggerInterest(cmd); break; case "world_log_feed": DoWorldLogFeed(cmd); break; case "domain_feed": DoDomainFeed(cmd); break; case "interest_inject": DoInterestInject(cmd); break; case "presentability_probe": DoPresentabilityProbe(cmd); break; case "interest_force_session": DoInterestForceSession(cmd); break; case "interest_combat_session": DoInterestCombatSession(cmd); break; case "interest_war_session": DoInterestWarSession(cmd); break; case "interest_earthquake_session": DoInterestEarthquakeSession(cmd); break; case "interest_crisis_begin": { bool ok = StoryPlanner.HarnessBeginCrisisFromCurrent(); if (ok) { _cmdOk++; } else { _cmdFail++; } CrisisChapter crisis = StoryPlanner.Crisis; Emit( cmd, ok, detail: ok ? $"crisis={crisis?.Kind} phase={crisis?.Phase}" : "no_crisis_tip"); break; } case "interest_crisis_end": { bool ok = StoryPlanner.HarnessForceCrisisEpilogue(); if (ok) { _cmdOk++; } else { _cmdFail++; } CrisisChapter crisis = StoryPlanner.Crisis; Emit( cmd, ok, detail: ok ? $"crisis={crisis?.Kind} phase={crisis?.Phase} epilogue=1" : "no_crisis_signal"); break; } case "interest_plot_session": DoInterestPlotSession(cmd); break; case "interest_family_session": DoInterestFamilySession(cmd); break; case "interest_outbreak_session": DoInterestOutbreakSession(cmd); break; case "combat_maintain_focus": { InterestDirector.HarnessMaintainCombatFocus(); // Prefer the candidate tip (Watch may debounce); LastWatchLabel can lag a hold. string tip = InterestDirector.CurrentCandidate?.Label ?? CameraDirector.LastWatchLabel ?? ""; string focus = FocusLabel(); _cmdOk++; Emit( cmd, ok: true, detail: $"key={InterestDirector.CurrentKey} tip='{tip}' focus={focus} completion={InterestDirector.CurrentCandidate?.Completion}"); break; } case "war_maintain_focus": { InterestDirector.HarnessMaintainWarFront(); string tip = CameraDirector.LastWatchLabel ?? ""; string focus = FocusLabel(); _cmdOk++; Emit( cmd, ok: true, detail: $"key={InterestDirector.CurrentKey} tip='{tip}' focus={focus} completion={InterestDirector.CurrentCandidate?.Completion}"); break; } case "plot_maintain_focus": { InterestDirector.HarnessMaintainPlotCell(); string tip = CameraDirector.LastWatchLabel ?? ""; string focus = FocusLabel(); _cmdOk++; Emit( cmd, ok: true, detail: $"key={InterestDirector.CurrentKey} tip='{tip}' focus={focus} completion={InterestDirector.CurrentCandidate?.Completion}"); break; } case "family_maintain_focus": { InterestDirector.HarnessMaintainFamilyPack(); string tip = CameraDirector.LastWatchLabel ?? ""; string focus = FocusLabel(); _cmdOk++; Emit( cmd, ok: true, detail: $"key={InterestDirector.CurrentKey} tip='{tip}' focus={focus} completion={InterestDirector.CurrentCandidate?.Completion}"); break; } case "outbreak_maintain_focus": { InterestDirector.HarnessMaintainStatusOutbreak(); string tip = CameraDirector.LastWatchLabel ?? ""; string focus = FocusLabel(); _cmdOk++; Emit( cmd, ok: true, detail: $"key={InterestDirector.CurrentKey} tip='{tip}' focus={focus} completion={InterestDirector.CurrentCandidate?.Completion}"); break; } case "combat_ensemble_escalate": DoCombatEnsembleEscalate(cmd); break; case "war_ensemble_apply": DoWarEnsembleApply(cmd); break; case "war_inject_theater_mass": DoWarInjectTheaterMass(cmd); break; case "plot_ensemble_apply": DoPlotEnsembleApply(cmd); break; case "family_ensemble_apply": DoFamilyEnsembleApply(cmd); break; case "outbreak_ensemble_apply": DoOutbreakEnsembleApply(cmd); break; case "combat_isolate_pair": DoCombatIsolatePair(cmd); break; case "combat_wire_attack_sides": DoCombatWireAttackSides(cmd); break; case "combat_wipe_sticky_side": DoCombatWipeStickySide(cmd); break; case "combat_set_sticky_side_count": DoCombatSetStickySideCount(cmd); break; case "combat_swap_attack_target": DoCombatSwapAttackTarget(cmd); break; case "combat_kill_related": DoCombatKillRelated(cmd); break; case "combat_probe_fighters": DoCombatProbeFighters(cmd); break; case "combat_park_bystander_follow": DoCombatParkBystanderFollow(cmd); break; case "interest_hijack_follow": DoInterestHijackFollow(cmd); break; case "interest_variety_clear": InterestVariety.Clear(); _cmdOk++; Emit(cmd, ok: true, detail: "variety_cleared"); break; case "editorial_replay_probe": { bool ok = InterestVariety.HarnessProbeEditorialReplay(out string detail); if (ok) _cmdOk++; else _cmdFail++; Emit(cmd, ok, detail: detail); break; } case "framing_approval_probe": { bool ok = EventPresentability.HarnessProbeFramingApproval(out string detail); if (ok) _cmdOk++; else _cmdFail++; Emit(cmd, ok, detail: detail); break; } case "interest_story_clear": StoryPlanner.Clear(); _cmdOk++; Emit(cmd, ok: true, detail: "story_cleared"); break; case "story_cross_arc_probe": { bool ok = StoryPlanner.HarnessProbeCrossArcCorrelation(out string detail); if (ok) _cmdOk++; else _cmdFail++; Emit(cmd, ok, detail: detail); break; } case "narrative_scheduler_probe": { bool ok = StoryScheduler.HarnessProbePolicy(out string detail); if (ok) _cmdOk++; else _cmdFail++; Emit(cmd, ok, detail: detail); break; } case "narrative_episode_grammar_probe": { bool ok = StoryScheduler.HarnessProbeGrammar(out string detail); if (ok) _cmdOk++; else _cmdFail++; Emit(cmd, ok, detail: detail); break; } case "narrative_ensemble_balance_probe": { bool ok = NarrativeExposureLedger.HarnessProbeBalance(out string detail); if (ok) _cmdOk++; else _cmdFail++; Emit(cmd, ok, detail: detail); break; } case "narrative_prose_redundancy_probe": { bool ok = NarrativeRenderer.HarnessProbeRelationshipProse(out string detail); if (ok) _cmdOk++; else _cmdFail++; Emit(cmd, ok, detail: detail + " coverage=" + NarrativePresentationCoverage.Summary); break; } case "narrative_iteration_probe": { bool contextOk = CaptionComposer.HarnessProbeContextPolicy(out string context); bool spineOk = WatchCaption.HarnessProbeStorySpineRelevance(out string spine); bool combatOk = StickyScoreboard.HarnessProbeReasonStability(out string combat); bool noveltyOk = InterestVariety.HarnessProbeRoutineReplay(out string novelty); bool familyCoalescingOk = InterestVariety.HarnessProbeFamilyCoalescing(out string familyCoalescing); bool nameBoundaryOk = UnitDossier.HarnessProbeNameBoundaries(out string nameBoundary); bool participantOk = NarrativePresentationFactory.HarnessProbeNamedParticipantFallback( out string participant); bool loverOk = HappinessEventRouter.HarnessProbeLiveLoverRecoveryPolicy(out string lover); bool boundedHappinessOk = HappinessEventRouter.HarnessProbeBoundedDrain(out string boundedHappiness); bool identityOk = UnitDossier.HarnessProbeIdentityJobPresentation(out string identity); bool episodeOk = StoryScheduler.HarnessProbeEpisodeContinuity(out string episode); bool proseOk = EventReason.HarnessProbePresentationProse(out string prose); bool lifecycleOk = NarrativeStoryStore.HarnessProbeCombatLifecycle(out string lifecycle); bool schedulerBoundsOk = NarrativeStoryStore.HarnessProbeSchedulerBounds(out string schedulerBounds); bool runtimeCompactionOk = NarrativeStoryStore.HarnessProbeRuntimeCompaction(out string runtimeCompaction); bool exactPresentationOk = CameraDirector.HarnessProbeExactPresentationReplay(out string exactPresentation); bool framingApprovalOk = EventPresentability.HarnessProbeFramingApproval(out string framingApproval); bool exposureOk = NarrativeExposureLedger.HarnessProbeBalance(out string exposure); bool interruptOk = InterestDirector.HarnessProbePersonalCombatInterrupt(out string interrupt); bool graphOk = NarrativePersistence.HarnessProbeGraphCompaction(out string graph); bool persistenceOk = NarrativePersistence.HarnessRoundTrip(out string persistence); bool ok = contextOk && spineOk && combatOk && noveltyOk && familyCoalescingOk && nameBoundaryOk && participantOk && loverOk && boundedHappinessOk && identityOk && episodeOk && proseOk && lifecycleOk && schedulerBoundsOk && runtimeCompactionOk && exactPresentationOk && framingApprovalOk && exposureOk && interruptOk && graphOk && persistenceOk; if (ok) _cmdOk++; else _cmdFail++; Emit( cmd, ok, detail: "context={" + context + "} spine={" + spine + "} combat={" + combat + "} novelty={" + novelty + "} familyCoalescing={" + familyCoalescing + "} nameBoundary={" + nameBoundary + "} participant={" + participant + "} lover={" + lover + "} boundedHappiness={" + boundedHappiness + "} identity={" + identity + "} episode={" + episode + "} prose={" + prose + "} lifecycle={" + lifecycle + "} schedulerBounds={" + schedulerBounds + "} runtimeCompaction={" + runtimeCompaction + "} exactPresentation={" + exactPresentation + "} framingApproval={" + framingApproval + "} exposure={" + exposure + "} interrupt={" + interrupt + "} graph={" + graph + "} persistence={" + persistence + "}"); break; } case "narrative_presentation_coverage_probe": { Actor subject = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : _lastSpawned; bool ok = EventFeedUtil.HarnessProbePresentationCoverage(subject, out string detail); if (ok) _cmdOk++; else _cmdFail++; Emit(cmd, ok, detail: detail); break; } case "narrative_relationship_ingestion_probe": { bool ok = false; string detail = ""; try { Actor parent = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : _lastSpawned; var others = new List(2); foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic()) { if (actor != null && actor.isAlive() && actor != parent && !others.Contains(actor)) { others.Add(actor); } if (others.Count >= 2) break; } if (parent == null || others.Count < 2) { detail = "missing actors parent=" + (parent != null) + " others=" + others.Count; } else { Actor firstChild = others[0]; Actor secondChild = others[1]; LifeSagaMemory.ClearSession(); NarrativeStoryStore.Clear(); CaptionComposer.Clear(); NarrativePresentationCoverage.Clear(); NarrativeEventReceipt first = LifeSagaMemory.RecordParentChild(parent, firstChild, "harness_ingestion"); NarrativeEventReceipt duplicate = LifeSagaMemory.RecordParentChild(parent, firstChild, "harness_ingestion_duplicate"); int eventCountAfterDuplicate = NarrativeEventStore.Count; int developmentCountAfterDuplicate = NarrativeStoryStore.DevelopmentCount; int consequenceCountAfterDuplicate = NarrativeStoryStore.CountConsequences( EventFeedUtil.SafeId(parent), CharacterConsequenceKind.BecameParent); NarrativeEventReceipt second = LifeSagaMemory.RecordParentChild(parent, secondChild, "harness_ingestion_second"); var tip = new InterestCandidate { Key = "harness:narrative:exact-child", Source = "harness", Category = "Family", Label = EventReason.NewChild(parent, firstChild), AssetId = "add_child", FollowUnit = parent, RelatedUnit = firstChild, SubjectId = EventFeedUtil.SafeId(parent), RelatedId = EventFeedUtil.SafeId(firstChild), NarrativeDevelopmentId = first.DevelopmentId, NarrativePresentation = first.Presentation?.Clone() }; CaptionModel caption = CaptionComposer.Compose( parent, tip, tip.Label, tip.RelatedId, "", statusBannerOwnsBeat: false); string firstName = EventFeedUtil.SafeName(firstChild); string secondName = EventFeedUtil.SafeName(secondChild); bool exactFirst = !string.IsNullOrEmpty(firstName) && caption.BeatLine.IndexOf( firstName, StringComparison.OrdinalIgnoreCase) >= 0 && (string.IsNullOrEmpty(secondName) || caption.BeatLine.IndexOf( secondName, StringComparison.OrdinalIgnoreCase) < 0); ok = first != null && first.IsNew && duplicate != null && !duplicate.IsNew && second != null && second.IsNew && eventCountAfterDuplicate == 1 && developmentCountAfterDuplicate == 1 && consequenceCountAfterDuplicate == 1 && NarrativeEventStore.Count == 2 && NarrativeStoryStore.DevelopmentCount == 2 && exactFirst && caption.ReasonRelatedId == EventFeedUtil.SafeId(firstChild) && !NarrativeRenderer.HasSemanticRedundancy( caption.BeatLine, first.Presentation) && NarrativePresentationCoverage.ExactDevelopmentCount == 1; detail = "first=" + first?.EventId + " duplicateNew=" + (duplicate?.IsNew ?? true) + " second=" + second?.EventId + " counts=" + eventCountAfterDuplicate + "/" + developmentCountAfterDuplicate + "/" + consequenceCountAfterDuplicate + " final=" + NarrativeEventStore.Count + "/" + NarrativeStoryStore.DevelopmentCount + " beat='" + caption.BeatLine + "'" + " related=" + caption.ReasonRelatedId + " coverage=" + NarrativePresentationCoverage.Summary; } } catch (Exception ex) { detail = "exception=" + ex.GetType().Name + ":" + ex.Message; } if (ok) _cmdOk++; else _cmdFail++; Emit(cmd, ok, detail: detail); break; } case "narrative_ingestion_matrix_probe": { bool ok = false; string detail = ""; try { Actor subject = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : _lastSpawned; Actor other = FindOtherAliveUnit(subject, "human"); if (subject == null || other == null) { detail = "missing actors subject=" + (subject != null) + " other=" + (other != null); } else { LifeSagaMemory.ClearSession(); NarrativeStoryStore.Clear(); var firsts = new List { LifeSagaMemory.RecordKill(subject, other, "harness_matrix"), LifeSagaMemory.RecordDeath(other, subject, "Harness", "harness_matrix"), LifeSagaMemory.RecordRole(subject, "harness_champion", "harness_matrix"), LifeSagaMemory.RecordHome(subject, true, "harness_matrix"), LifeSagaMemory.RecordHome(subject, false, "harness_matrix"), LifeSagaMemory.RecordFounding( subject, "city", "harness_city", "harness_matrix"), LifeSagaMemory.RecordWar( subject, "harness_war", "harness_kingdom", false, "harness_matrix", other), LifeSagaMemory.RecordWar( subject, "harness_war", "harness_kingdom", true, "harness_matrix", other), LifeSagaMemory.RecordPlot( subject, "harness_plot", "harness_plot_asset", false, "harness_matrix", other), LifeSagaMemory.RecordPlot( subject, "harness_plot", "harness_plot_asset", true, "harness_matrix", other) }; var duplicates = new List { LifeSagaMemory.RecordKill(subject, other, "harness_matrix_duplicate"), LifeSagaMemory.RecordDeath( other, subject, "Harness", "harness_matrix_duplicate"), LifeSagaMemory.RecordRole( subject, "harness_champion", "harness_matrix_duplicate"), LifeSagaMemory.RecordHome(subject, true, "harness_matrix_duplicate"), LifeSagaMemory.RecordHome(subject, false, "harness_matrix_duplicate"), LifeSagaMemory.RecordFounding( subject, "city", "harness_city", "harness_matrix_duplicate"), LifeSagaMemory.RecordWar( subject, "harness_war", "harness_kingdom", false, "harness_matrix_duplicate", other), LifeSagaMemory.RecordWar( subject, "harness_war", "harness_kingdom", true, "harness_matrix_duplicate", other), LifeSagaMemory.RecordPlot( subject, "harness_plot", "harness_plot_asset", false, "harness_matrix_duplicate", other), LifeSagaMemory.RecordPlot( subject, "harness_plot", "harness_plot_asset", true, "harness_matrix_duplicate", other) }; bool firstsNew = true; bool duplicatesOld = true; bool exactLinks = true; for (int i = 0; i < firsts.Count; i++) { NarrativeEventReceipt first = firsts[i]; NarrativeEventReceipt duplicate = duplicates[i]; firstsNew &= first != null && first.IsNew; duplicatesOld &= duplicate != null && !duplicate.IsNew; exactLinks &= first != null && !string.IsNullOrEmpty(first.EventId) && !string.IsNullOrEmpty(first.DevelopmentId) && NarrativeEventStore.Get(first.EventId) != null && NarrativeStoryStore.Development(first.DevelopmentId) != null && duplicate != null && duplicate.EventId == first.EventId && duplicate.DevelopmentId == first.DevelopmentId; } ok = firstsNew && duplicatesOld && exactLinks && NarrativeEventStore.Count == firsts.Count && NarrativeStoryStore.DevelopmentCount == firsts.Count; detail = "new=" + firstsNew + " duplicateOld=" + duplicatesOld + " exact=" + exactLinks + " events=" + NarrativeEventStore.Count + " developments=" + NarrativeStoryStore.DevelopmentCount + " expected=" + firsts.Count; } } catch (Exception ex) { detail = "exception=" + ex.GetType().Name + ":" + ex.Message; } if (ok) _cmdOk++; else _cmdFail++; Emit(cmd, ok, detail: detail); break; } case "narrative_persistence_probe": { Actor subject = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : _lastSpawned; Actor other = FindOtherAliveUnit(subject, "human"); bool ok = NarrativePersistence.HarnessRestoreRoundTrip( subject, other, out string detail); if (ok) _cmdOk++; else _cmdFail++; Emit(cmd, ok, detail: detail); break; } case "narrative_persistence_invalid_probe": { bool ok = NarrativePersistence.HarnessRejectsInvalid(out string detail); if (ok) _cmdOk++; else _cmdFail++; Emit(cmd, ok, detail: detail); break; } case "narrative_camera_cutover_probe": { bool ok = false; string detail = ""; try { Actor protagonist = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : _lastSpawned; Actor child = FindOtherAliveUnit(protagonist, "human"); Actor outsider = null; foreach (Actor candidate in WorldActivityScanner.EnumerateAliveUnitsPublic()) { if (candidate != null && candidate.isAlive() && candidate != protagonist && candidate != child) { outsider = candidate; break; } } if (protagonist == null || child == null || outsider == null) { detail = "missing actors"; } else { InterestDirector.HarnessEndCurrent("narrative_cutover_setup"); InterestRegistry.Clear(); NarrativeStoryStore.Clear(); NarrativeDevelopmentRouter.ChildBorn(protagonist, child, "harness_cutover"); InterestCandidate unrelated = HarnessNarrativeCandidate( "harness:narrative:unrelated", outsider, "Unrelated high event", 90f); InterestCandidate related = HarnessNarrativeCandidate( "harness:narrative:related", protagonist, "Lineage develops", 60f); related.AssetId = "add_child"; EventFeedUtil.RegisterCandidate(unrelated); EventFeedUtil.RegisterCandidate(related); StoryScheduler.Tick(Time.unscaledTime + 1f); InterestCandidate first = InterestDirector.HarnessPeekNext(); bool relatedWon = first != null && first.Key == related.Key; bool watched = relatedWon && InterestDirector.HarnessForceSession(first); InterestDirector.HarnessAgeCurrent(20f); InterestCandidate critical = HarnessNarrativeCandidate( "harness:narrative:critical", outsider, "Kingdom shattered", 100f); critical.Category = "Disaster"; EventFeedUtil.RegisterCandidate(critical); StoryScheduler.Tick(Time.unscaledTime + 2f); InterestCandidate second = InterestDirector.HarnessPeekNext(); bool criticalWon = second != null && second.Key == critical.Key; ok = relatedWon && watched && criticalWon; detail = "first=" + (first?.Key ?? "none") + " watched=" + watched + " second=" + (second?.Key ?? "none") + " episode=" + (StoryScheduler.ActiveEpisode?.ThreadId ?? "none") + " pass=" + ok; } } catch (Exception ex) { detail = "exception=" + ex.GetType().Name + ":" + ex.Message; ok = false; } if (ok) _cmdOk++; else _cmdFail++; Emit(cmd, ok, detail: detail); break; } case "narrative_story_inputs_probe": { Actor protagonist = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : _lastSpawned; Actor child = FindOtherAliveUnit(protagonist, "human"); bool recorded = false; bool admitted = false; float potential = 0f; if (protagonist != null && child != null) { NarrativeStoryStore.Clear(); LifeSagaRoster.Clear(); recorded = NarrativeDevelopmentRouter.ChildBorn(protagonist, child, "harness_story_inputs"); potential = StoryScheduler.StoryPotential(EventFeedUtil.SafeId(protagonist)); LifeSagaRoster.Tick(Time.unscaledTime + 2f); LifeSagaSlot slot = LifeSagaRoster.Get(EventFeedUtil.SafeId(protagonist)); admitted = slot != null && slot.AdmissionReason == LifeSagaAdmissionReason.EmergingStory; } var consequence = new InterestCandidate { AssetId = "add_child", Category = "Family", EventStrength = 52f }; var escalation = new InterestCandidate { Completion = InterestCompletionKind.WarFront, Category = "War", EventStrength = 65f }; var texture = new InterestCandidate { LeadKind = InterestLeadKind.CharacterLed, Completion = InterestCompletionKind.CharacterVignette, EventStrength = 80f }; NarrativeFunction cf = NarrativeFunctionPolicy.Classify(consequence); NarrativeFunction ef = NarrativeFunctionPolicy.Classify(escalation); NarrativeFunction tf = NarrativeFunctionPolicy.Classify(texture); bool ok = recorded && potential >= 6f && admitted && cf == NarrativeFunction.Consequence && ef == NarrativeFunction.Escalation && tf == NarrativeFunction.CharacterTexture; if (ok) _cmdOk++; else _cmdFail++; Emit(cmd, ok, detail: "recorded=" + recorded + " potential=" + potential.ToString("0.0") + " admitted=" + admitted + " functions=" + cf + "/" + ef + "/" + tf); break; } case "narrative_soak_seed": { Actor protagonist = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : _lastSpawned; Actor child = FindOtherAliveUnit(protagonist, "human"); bool ok = protagonist != null && child != null; if (ok) { InterestDirector.HarnessEndCurrent("narrative_soak_seed"); InterestRegistry.Clear(); NarrativeStoryStore.Clear(); NarrativeDevelopmentRouter.ChildBorn(protagonist, child, "harness_story_soak"); InterestCandidate related = HarnessNarrativeCandidate( "harness:narrative:add_child", protagonist, "Welcomes a child", 58f); related.AssetId = "add_child"; related.Category = "Family"; EventFeedUtil.RegisterCandidate(related); } if (ok) _cmdOk++; else _cmdFail++; Emit(cmd, ok, detail: "seeded=" + ok + " protagonist=" + EventFeedUtil.SafeId(protagonist) + " child=" + EventFeedUtil.SafeId(child)); break; } case "narrative_telemetry_probe": { bool ok = NarrativeTelemetry.Count > 0 && NarrativeTelemetry.RelatedProposalCount > 0 && NarrativeTelemetry.ComparedCount > 0 && NarrativeTelemetry.RelatedActualCount > 0; if (ok) _cmdOk++; else _cmdFail++; Emit(cmd, ok, detail: NarrativeTelemetry.Summary + " episode=" + (StoryScheduler.ActiveEpisode?.ThreadId ?? "none")); break; } case "narrative_catalog_probe": { NarrativeFunctionCatalog.ClearCoverage(); var child = new InterestCandidate { Key = "catalog:child", AssetId = "add_child" }; var war = new InterestCandidate { Key = "catalog:war", AssetId = "live_war", Completion = InterestCompletionKind.WarFront }; var daily = new InterestCandidate { Key = "catalog:daily", HappinessEffectId = "just_slept" }; var disaster = new InterestCandidate { Key = "catalog:disaster", AssetId = "disaster_tornado" }; NarrativeFunctionCatalog.Stamp(child); NarrativeFunctionCatalog.Stamp(war); NarrativeFunctionCatalog.Stamp(daily); NarrativeFunctionCatalog.Stamp(disaster); bool ok = child.NarrativeFunction == NarrativeFunction.Consequence && war.NarrativeFunction == NarrativeFunction.Escalation && daily.NarrativeFunction == NarrativeFunction.CharacterTexture && disaster.NarrativeFunction == NarrativeFunction.WorldContext && NarrativeFunctionCatalog.ExplicitCount == 4 && NarrativeFunctionCatalog.FallbackCount == 0; if (ok) _cmdOk++; else _cmdFail++; Emit(cmd, ok, detail: NarrativeFunctionCatalog.CoverageSummary); break; } case "narrative_natural_soak_begin": InterestDirector.HarnessEndCurrent("narrative_natural_soak"); InterestRegistry.Clear(); NarrativeStoryStore.Clear(); NarrativeFunctionCatalog.ClearCoverage(); NarrativeTelemetry.Clear(); _cmdOk++; Emit(cmd, ok: true, detail: "natural_soak_reset"); break; case "narrative_natural_soak_probe": { int typed = NarrativeFunctionCatalog.ExplicitCount + NarrativeFunctionCatalog.StructuralCount; float typedRatio = NarrativeFunctionCatalog.ObservedCount > 0 ? typed / (float)NarrativeFunctionCatalog.ObservedCount : 0f; bool cameraHealthy = MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null && MoveCamera._focus_unit.isAlive(); bool ok = NarrativeFunctionCatalog.ObservedCount > 0 && typed > 0 && typedRatio >= 0.25f && cameraHealthy; if (ok) _cmdOk++; else _cmdFail++; Emit(cmd, ok, detail: NarrativeFunctionCatalog.CoverageSummary + " typedRatio=" + typedRatio.ToString("0.00") + " cameraHealthy=" + cameraHealthy + " telemetry=" + NarrativeTelemetry.Summary + " episode=" + (StoryScheduler.ActiveEpisode?.ThreadId ?? "none")); break; } case "narrative_multithread_probe": { bool ok = false; string detail = ""; try { var actors = new List(6); foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic()) { if (actor != null && actor.isAlive() && !actors.Contains(actor)) actors.Add(actor); if (actors.Count >= 5) break; } if (actors.Count < 5) { detail = "actors=" + actors.Count; } else { Actor a = actors[0]; Actor childA = actors[1]; Actor b = actors[2]; Actor childB = actors[3]; Actor outsider = actors[4]; long aid = EventFeedUtil.SafeId(a); long bid = EventFeedUtil.SafeId(b); InterestDirector.HarnessEndCurrent("narrative_multithread_setup"); InterestRegistry.Clear(); NarrativeStoryStore.Clear(); LifeSagaRoster.Clear(); LifeSagaRoster.HarnessForceAdmit(a); LifeSagaRoster.SetPrefer(aid, true); NarrativeDevelopmentRouter.ChildBorn(a, childA, "harness_multithread_a"); NarrativeDevelopmentRouter.ChildBorn(b, childB, "harness_multithread_b"); InterestCandidate shotA = HarnessNarrativeCandidate( "harness:multi:a", a, "Lineage A develops", 62f); shotA.AssetId = "add_child"; InterestCandidate shotB = HarnessNarrativeCandidate( "harness:multi:b", b, "Lineage B develops", 68f); shotB.AssetId = "add_child"; EventFeedUtil.RegisterCandidate(shotA); EventFeedUtil.RegisterCandidate(shotB); float now = Time.unscaledTime; StoryScheduler.Tick(now + 1f); string firstThread = StoryScheduler.ActiveEpisode?.ThreadId ?? ""; InterestCandidate first = InterestDirector.HarnessPeekNext(); bool startsA = StoryScheduler.ActiveEpisode?.ProtagonistId == aid && first?.Key == shotA.Key; bool watchedA = startsA && InterestDirector.HarnessForceSession(first); NarrativeDevelopmentRouter.Role(b, "become_king", true, "harness_multithread_b"); StoryScheduler.Tick(now + 2f); bool retainedA = StoryScheduler.ActiveEpisode?.ThreadId == firstThread; InterestCandidate critical = HarnessNarrativeCandidate( "harness:multi:critical", outsider, "Kingdom shattered", 100f); critical.Category = "Disaster"; EventFeedUtil.RegisterCandidate(critical); InterestDirector.HarnessAgeCurrent(20f); StoryScheduler.Tick(now + 3f); InterestCandidate criticalPick = InterestDirector.HarnessPeekNext(); bool criticalWon = criticalPick?.Key == critical.Key; bool watchedCritical = criticalWon && InterestDirector.HarnessForceSession(criticalPick); bool survivedInterrupt = StoryScheduler.ActiveEpisode?.ThreadId == firstThread; InterestDirector.HarnessEndCurrent("critical_complete"); InterestCandidate resumeA = HarnessNarrativeCandidate( "harness:multi:a:resume", a, "Lineage A continues", 64f); resumeA.AssetId = "add_child"; EventFeedUtil.RegisterCandidate(resumeA); StoryScheduler.Tick(now + 4f); InterestCandidate resumed = StoryScheduler.SelectForCamera( new List { resumeA }, current: null, now: now + 4f); bool resumedA = resumed?.Key == resumeA.Key && StoryScheduler.ActiveEpisode?.ThreadId == firstThread; NarrativeThread closing = NarrativeStoryStore.Thread(firstThread); if (closing != null) closing.Status = NarrativeThreadStatus.Abandoned; StoryScheduler.Tick(now + 20f); bool handedToB = StoryScheduler.ActiveEpisode?.ProtagonistId == bid; ok = startsA && watchedA && retainedA && criticalWon && watchedCritical && survivedInterrupt && resumedA && handedToB; detail = "startsA=" + startsA + " retainedA=" + retainedA + " critical=" + criticalWon + "/" + watchedCritical + " survived=" + survivedInterrupt + " resumedA=" + resumedA + " handedToB=" + handedToB + " firstThread=" + firstThread + " finalThread=" + (StoryScheduler.ActiveEpisode?.ThreadId ?? "none"); } } catch (Exception ex) { detail = "exception=" + ex.GetType().Name + ":" + ex.Message; ok = false; } if (ok) _cmdOk++; else _cmdFail++; Emit(cmd, ok, detail: detail); break; } case "caption_status_banner_probe": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; long id = EventFeedUtil.SafeId(focus); string beforeIdentity = WatchCaption.LastHeadline ?? ""; string beforeContext = WatchCaption.LastContextLine ?? ""; bool prepared = focus != null && id != 0 && LifeSagaRoster.IsMc(id); if (prepared) { InterestDirector.PauseForBrowsing("Harness status banner"); prepared = LifeSagaRoster.HarnessSimulateRoleRise(focus, "become_king"); LifeSagaMemory.RecordWar( focus, "harness-banner-war", "northreach", ended: false, provenance: "harness_banner", note: "Northreach"); WatchCaption.Update(); } string identity = WatchCaption.LastHeadline ?? ""; string context = WatchCaption.LastContextLine ?? ""; string visibleBeat = WatchCaption.VisibleBeatLine ?? ""; bool ok = prepared && WatchCaption.HasStatusBannerActive() && visibleBeat.IndexOf("Harness status banner", StringComparison.Ordinal) >= 0 && identity.IndexOf("King", StringComparison.OrdinalIgnoreCase) >= 0 && !string.Equals(identity, beforeIdentity, StringComparison.Ordinal) && !string.IsNullOrEmpty(context) && !string.Equals(context, beforeContext, StringComparison.Ordinal); if (ok) _cmdOk++; else _cmdFail++; Emit( cmd, ok, detail: $"prepared={prepared} banner={WatchCaption.HasStatusBannerActive()} beat='{visibleBeat}' " + $"beforeIdentity='{beforeIdentity}' identity='{identity}' beforeContext='{beforeContext}' context='{context}'"); break; } case "saga_relation_cache_revision_probe": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; long id = EventFeedUtil.SafeId(focus); bool prepared = focus != null && id != 0; LifeSagaMemory.Clear(); int r0 = SagaProse.MemoryRevision; if (prepared) { LifeSagaMemory.RecordWar( focus, "harness-revision-update", "north", ended: false, provenance: "harness_revision"); } int r1 = SagaProse.MemoryRevision; if (prepared) { // Same correlation key mutates the existing fact. LifeSagaMemory.RecordWar( focus, "harness-revision-update", "north", ended: true, provenance: "harness_revision"); } int r2 = SagaProse.MemoryRevision; if (prepared) { LifeSagaMemory.RecordWar( focus, "harness-revision-resolve", "south", ended: false, provenance: "harness_revision"); } int r3 = SagaProse.MemoryRevision; LifeSagaMemory.ResolveWarJoins("harness-revision-resolve"); int r4 = SagaProse.MemoryRevision; long rivalId = 987654321; if (prepared) { LifeSagaMemory.RecordIds( LifeSagaFactKind.RivalEarned, id, new LifeSagaIdentity { Id = id, Name = EventFeedUtil.SafeName(focus), SpeciesId = "human" }, rivalId, new LifeSagaIdentity { Id = rivalId, Name = "Weak Rival", SpeciesId = "human" }, correlationKey: "rival:harness-revision", strength: 20f, provenance: "newKillAction", note: "repeated_encounters:1"); } int r5 = SagaProse.MemoryRevision; LifeSagaMemory.TryGetEarnedRival(id, out _); int r6 = SagaProse.MemoryRevision; bool ok = prepared && r1 > r0 && r2 > r1 && r4 > r3 && r6 > r5; if (ok) _cmdOk++; else _cmdFail++; Emit(cmd, ok, detail: $"prepared={prepared} revisions={r0},{r1},{r2},{r3},{r4},{r5},{r6}"); break; } case "saga_cast_dedupe_probe": { bool ok = LifeSagaPanel.HarnessProbeScopedCastDedup(out string detail); if (ok) _cmdOk++; else _cmdFail++; Emit(cmd, ok, detail: detail); break; } case "interest_saga_clear": LifeSagaRoster.Clear(); LifeSagaMemory.Clear(); LifeSagaViewController.Clear(); LifeSagaRail.Clear(); LifeSagaPanel.Clear(); _sagaPanelSizeAnchor = Vector2.zero; _cmdOk++; Emit(cmd, ok: true, detail: "saga_cleared"); break; case "saga_session_reset": { long sentinelId = EventFeedUtil.SafeId( MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null); LifeSagaSession.ResetForWorldChange("harness"); _sagaPanelSizeAnchor = Vector2.zero; bool cleared = sentinelId == 0 || (!LifeSagaRoster.IsMc(sentinelId) && LifeSagaMemory.Get(sentinelId) == null); if (cleared) _cmdOk++; else _cmdFail++; Emit( cmd, ok: cleared, detail: $"saga_session_reset key='{LifeSagaSession.BoundKey}' epoch={LifeSagaSession.Epoch} sentinel={sentinelId} cleared={cleared} livesNow={LifeSagaMemory.LifeCount}"); break; } case "interest_story_purge_leftovers": StoryPlanner.PurgeCloserLeftovers(); _cmdOk++; Emit( cmd, ok: true, detail: $"purged crisisActive={StoryPlanner.CrisisActive} softQuiet={StoryPlanner.SoftFillQuietActive}"); break; case "story_resume_parked": { var board = new System.Collections.Generic.List(4); StoryPlanner.CopyBoard(board); string id = ""; for (int i = 0; i < board.Count; i++) { if (board[i] != null && board[i].IsParked) { id = board[i].Id ?? ""; break; } } bool ok = !string.IsNullOrEmpty(id) && StoryPlanner.TryResume(id); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: $"resumeOk={ok} id='{id}' watching='{StoryPlanner.Active?.Id}' board={StoryPlanner.BoardCount}"); break; } case "saga_prefer_focus": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; long id = EventFeedUtil.SafeId(focus); LifeSagaRoster.Tick(Time.unscaledTime); bool ok = id != 0 && LifeSagaRoster.IsMc(id) && LifeSagaRoster.TogglePrefer(id); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: $"preferToggle id={id} onRoster={LifeSagaRoster.IsMc(id)} prefer={LifeSagaRoster.IsPrefer(id)}"); break; } case "saga_force_admit_focus": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; long id = EventFeedUtil.SafeId(focus); bool ok = LifeSagaRoster.HarnessForceAdmit(focus); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: $"admit id={id} roster={LifeSagaRoster.Count} on={ok}"); break; } case "saga_fill_cap": { // Force-admit living units until Cap (skips Prefer partner / current focus when asked). string assetId = string.IsNullOrEmpty(cmd.asset) || cmd.asset == "auto" ? "human" : cmd.asset; Actor skip = _happinessPartner; int added = 0; int guard = 0; while (LifeSagaRoster.Count < LifeSagaRoster.Cap && guard++ < 40) { Actor unit = FindOtherAliveUnitPreferOffRoster(skip, assetId, preferOffRoster: true); if (unit == null || !unit.isAlive()) { break; } if (!LifeSagaRoster.HarnessForceAdmit(unit)) { break; } added++; skip = unit; } bool ok = LifeSagaRoster.Count >= LifeSagaRoster.Cap; if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"filled added={added} roster={LifeSagaRoster.Count} cap={LifeSagaRoster.Cap}"); break; } case "saga_force_admit_partner": { Actor partner = _happinessPartner; long id = EventFeedUtil.SafeId(partner); bool ok = LifeSagaRoster.HarnessForceAdmit(partner); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: $"admitPartner id={id} roster={LifeSagaRoster.Count} on={ok}"); break; } case "saga_consider_admit_focus": { Actor actor = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; long id = EventFeedUtil.SafeId(actor); bool ok = id != 0 && LifeSagaRoster.ConsiderAdmit(id, heat: 0f); if (ok) _cmdOk++; else _cmdFail++; Emit( cmd, ok, detail: $"consider id={id} asset={actor?.asset?.id} roster={LifeSagaRoster.Count} on={LifeSagaRoster.IsMc(id)}"); break; } case "saga_consider_antagonist_partner": { Actor partner = _happinessPartner; long id = EventFeedUtil.SafeId(partner); bool eligible = LifeSagaRoster.TryGetRecurringAntagonist( partner, out long anchor, out LifeSagaFact evidence); bool ok = eligible && LifeSagaRoster.ConsiderAdmit(id, heat: 0f); if (ok) _cmdOk++; else _cmdFail++; Emit( cmd, ok, detail: $"antagonist={id} eligible={eligible} anchor={anchor} evidence={evidence?.Note} on={LifeSagaRoster.IsMc(id)}"); break; } case "saga_note_encounters": { Actor a = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; Actor b = _happinessPartner; int times = 3; if (!string.IsNullOrEmpty(cmd.value) && int.TryParse(cmd.value.Trim(), out int parsed)) { times = Mathf.Max(1, parsed); } bool ok = a != null && b != null && a != b; if (ok) { // Bypass cooldown by clearing pair cool via Clear would wipe memory - call with // distinct provenance and temporarily force by calling NoteCombatEncounter // after clearing only PairEncounterCooldown through repeated ClearSession is bad. // Use EarnRival path for rematch: clear cooldown by noting with spaced calls. for (int i = 0; i < times; i++) { LifeSagaMemory.HarnessNoteCombatEncounter(a, b, "harness_rematch:" + i); } _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: $"encounters={times} a={SafeName(a)} b={SafeName(b)}"); break; } case "saga_record_kill_partner": { Actor killer = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; Actor victim = _happinessPartner; bool ok = killer != null && victim != null && killer != victim; if (ok) { LifeSagaMemory.RecordKill(killer, victim, "harness_kill"); LifeSagaMemory.RecordDeath(victim, killer, "harness", "harness_death"); _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: $"kill {SafeName(killer)} -> {SafeName(victim)}"); break; } case "saga_legacy_combat_probe": { Actor killer = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; int recorded = 0; if (killer != null && killer.isAlive() && World.world?.units != null) { try { foreach (Actor victim in World.world.units) { if (victim == null || !victim.isAlive() || victim == killer) { continue; } LifeSagaMemory.RecordKill( killer, victim, "harness_combat_summary:" + recorded); recorded++; if (recorded >= 3) break; } } catch { // failure is reported by the assertions below } } LifeSagaViewModel model = LifeSagaPresentation.Build(EventFeedUtil.SafeId(killer)); int summaryCount = 0; int rawKillCount = 0; string lines = ""; if (model != null) { for (int i = 0; i < model.Legacy.Count; i++) { string line = model.Legacy[i]?.Line ?? ""; if (string.IsNullOrEmpty(line)) continue; if (lines.Length > 0) lines += " | "; lines += line; if (line.IndexOf( "Slew 3 opponents across repeated clashes", StringComparison.OrdinalIgnoreCase) >= 0) { summaryCount++; } else if (model.Legacy[i].Kind == LifeSagaFactKind.Kill) { rawKillCount++; } } } bool ok = recorded == 3 && summaryCount == 1 && rawKillCount == 0; if (ok) _cmdOk++; else _cmdFail++; Emit( cmd, ok, detail: $"recorded={recorded} summary={summaryCount} rawKills={rawKillCount}" + $" legacy='{lines}'"); break; } case "interest_bind_related_partner": { InterestCandidate cur = InterestDirector.CurrentCandidate; Actor partner = _happinessPartner; string needle = (cmd.expect ?? "").Trim(); if (string.IsNullOrEmpty(needle)) { needle = (cmd.label ?? "").Trim(); } if (string.IsNullOrEmpty(needle)) { needle = (cmd.value ?? "").Trim(); } if (!string.IsNullOrEmpty(needle) && (cur == null || string.IsNullOrEmpty(cur.Label) || cur.Label.IndexOf(needle, StringComparison.OrdinalIgnoreCase) < 0)) { var pending = new System.Collections.Generic.List(64); InterestRegistry.CopyPending(pending); for (int i = 0; i < pending.Count; i++) { InterestCandidate p = pending[i]; if (p == null || string.IsNullOrEmpty(p.Label)) { continue; } if (p.Label.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0) { cur = p; break; } } } bool ok = cur != null && partner != null && partner.isAlive(); if (ok) { cur.RelatedUnit = partner; cur.RelatedId = EventFeedUtil.SafeId(partner); InterestScoring.RecalcTotal(cur); _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: $"bind related={SafeName(partner)} tip={cur?.Label} score={cur?.TotalScore:0.#} needle='{needle}'"); break; } case "saga_stamp_chapter_focus": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; long id = EventFeedUtil.SafeId(focus); string line = (cmd.value ?? "").Trim(); if (string.IsNullOrEmpty(line)) { line = (cmd.label ?? "").Trim(); } LifeSagaChapterKind kind = LifeSagaChapterKind.Unknown; string kindRaw = (cmd.asset ?? "").Trim(); if (!string.IsNullOrEmpty(kindRaw) && System.Enum.TryParse(kindRaw, ignoreCase: true, out LifeSagaChapterKind parsed)) { kind = parsed; } bool ok = id != 0 && LifeSagaRoster.IsMc(id) && !string.IsNullOrEmpty(line); if (ok) { LifeSagaRoster.StampChapter(id, kind, line); LifeSagaRoster.Dirty = true; _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"stamp id={id} kind={kind} line={line} chapters={LifeSagaRoster.Get(id)?.Chapters.Count ?? 0}"); break; } case "saga_weaken_nonprefer": { // Age out non-Prefer slots so a hotter admit can displace them. float now = Time.unscaledTime; var board = new System.Collections.Generic.List(LifeSagaRoster.Cap); LifeSagaRoster.CopySlots(board); int n = 0; for (int i = 0; i < board.Count; i++) { LifeSagaSlot s = board[i]; if (s == null || s.Prefer || s.GameFavorite) { continue; } LifeSagaRoster.HarnessSetScores(s.UnitId, standing: 1f, heat: 0f); s.AdmittedAt = now - 900f; s.TouchedAt = now - 900f; s.LastChapterAt = 0f; n++; } LifeSagaRoster.Dirty = true; _cmdOk++; Emit(cmd, ok: true, detail: $"weakened={n} roster={LifeSagaRoster.Count}"); break; } case "saga_stiffen_roster": { // Keep Cap competitive via Heat (RefreshSlot rewrites Standing from live BaseInterest). float now = Time.unscaledTime; float heat = LifeSagaRoster.HeatCap; if (!string.IsNullOrEmpty(cmd.value) && float.TryParse( cmd.value.Trim(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float parsed) && parsed > 0f) { heat = parsed; } var board = new System.Collections.Generic.List(LifeSagaRoster.Cap); LifeSagaRoster.CopySlots(board); int n = 0; for (int i = 0; i < board.Count; i++) { LifeSagaSlot s = board[i]; if (s == null || s.UnitId == 0) { continue; } // Standing is overwritten on Tick; heat is the durable harness lever. LifeSagaRoster.HarnessSetScores(s.UnitId, standing: 1f, heat: heat); s.AdmittedAt = now; s.TouchedAt = now; s.LastChapterAt = now; n++; } LifeSagaRoster.Dirty = true; _cmdOk++; Emit(cmd, ok: true, detail: $"stiffened={n} heat={heat:0.##} roster={LifeSagaRoster.Count}"); break; } case "saga_rail_refresh": { LifeSagaRoster.Tick(Time.unscaledTime); LifeSagaRail.Refresh(); _cmdOk++; Emit( cmd, ok: true, detail: $"railShown={LifeSagaRail.LastShownCount} activeLit={LifeSagaRail.LastActiveHighlight} preferPip={LifeSagaRail.LastPreferPipVisible}"); break; } case "caption_capture_beat": _captionBeatAnchor = WatchCaption.VisibleBeatLine ?? ""; _cmdOk++; Emit(cmd, ok: true, detail: $"beat='{_captionBeatAnchor}'"); break; case "saga_show_hover_first": { LifeSagaRoster.Tick(Time.unscaledTime); // Soft-admit focus if the roster was pruned mid-scenario (unit death / // bind churn) so hover depth stays testable on the watched life. if (LifeSagaRoster.Count == 0) { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; if (focus != null && focus.isAlive()) { LifeSagaRoster.HarnessForceAdmit(focus); } } LifeSagaRail.Refresh(); LifeSagaRail.ShowHover(0); WatchCaption.RequestRelayout(); LifeSagaViewModel model = LifeSagaViewController.BuildEffectivePresentation(); // Height may be 0 when Cast+Legacy are empty; hover is still valid. bool ok = LifeSagaViewController.IsHoverPreview && model != null && model.UnitId != 0 && LifeSagaViewController.HoverPreviewId != 0; if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"hoverId={LifeSagaViewController.HoverPreviewId} rows={LifeSagaRail.LastHoverRowCount} height={LifeSagaRail.LastHoverHeight:0.0} pause={InterestDirector.ReadPauseActive} rail={LifeSagaRail.LastShownCount} roster={LifeSagaRoster.Count}"); break; } case "saga_hide_hover": { LifeSagaRail.HideHover(); WatchCaption.RequestRelayout(); _cmdOk++; Emit(cmd, ok: true, detail: $"pause={InterestDirector.ReadPauseActive}"); break; } case "saga_hover_tick_away": { LifeSagaViewController.HarnessTickExitAwayFromChrome(); _cmdOk++; Emit( cmd, ok: true, detail: $"hoverPreview={LifeSagaViewController.IsHoverPreview} pause={InterestDirector.ReadPauseActive}"); break; } case "saga_click_first": { LifeSagaRail.Refresh(); long focusId = EventFeedUtil.SafeId(MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null); bool preferBefore = focusId != 0 && LifeSagaRoster.IsPrefer(focusId); LifeSagaRail.HarnessClick(0); WatchCaption.RequestRelayout(); // Prefer toggle only - does not open hover / pin saga subject. bool preferAfter = LifeSagaRail.LastPreferPipVisible || (focusId != 0 && LifeSagaRoster.IsPrefer(focusId)); bool ok = preferAfter || preferBefore != (focusId != 0 && LifeSagaRoster.IsPrefer(focusId)); // After click, Prefer state on slot 0 should be readable. var slots = new System.Collections.Generic.List(LifeSagaRoster.Cap); LifeSagaRoster.CopySlots(slots); if (slots.Count > 0 && slots[0] != null) { ok = slots[0].Prefer || preferBefore; } if (ok) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: $"preferBefore={preferBefore} preferAfter={preferAfter} hover={LifeSagaViewController.IsHoverPreview}"); break; } case "saga_select_tab": { string raw = (cmd.value ?? "dossier").Trim().ToLowerInvariant(); LifeSagaViewController.Tab tab = raw.StartsWith("s") ? LifeSagaViewController.Tab.Saga : LifeSagaViewController.Tab.Dossier; LifeSagaViewController.SelectTab(tab); WatchCaption.RequestRelayout(); _cmdOk++; Emit(cmd, ok: true, detail: $"tab={LifeSagaViewController.SelectedTab}"); break; } case "saga_memory_record_focus": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; if (focus == null || !focus.isAlive()) { _cmdFail++; Emit(cmd, ok: false, detail: "no_focus"); break; } string kindRaw = (cmd.asset ?? "Kill").Trim(); LifeSagaFactKind kind = LifeSagaFactKind.Kill; if (kindRaw.IndexOf("bond", StringComparison.OrdinalIgnoreCase) >= 0) { kind = LifeSagaFactKind.BondFormed; } else if (kindRaw.IndexOf("war", StringComparison.OrdinalIgnoreCase) >= 0) { kind = LifeSagaFactKind.WarJoin; } else if (kindRaw.IndexOf("plot", StringComparison.OrdinalIgnoreCase) >= 0) { kind = LifeSagaFactKind.PlotJoin; } else if (kindRaw.IndexOf("found", StringComparison.OrdinalIgnoreCase) >= 0) { kind = LifeSagaFactKind.Founding; } else if (kindRaw.IndexOf("role", StringComparison.OrdinalIgnoreCase) >= 0 || kindRaw.IndexOf("king", StringComparison.OrdinalIgnoreCase) >= 0) { kind = LifeSagaFactKind.RoleChange; } else if (kindRaw.IndexOf("parent", StringComparison.OrdinalIgnoreCase) >= 0 || kindRaw.IndexOf("child", StringComparison.OrdinalIgnoreCase) >= 0) { kind = LifeSagaFactKind.ParentChild; } Actor other = _happinessPartner; if (other == focus) { other = null; } LifeSagaFact fact; if (kind == LifeSagaFactKind.WarJoin) { string warKey = string.IsNullOrEmpty(cmd.expect) ? "harness_war" : cmd.expect.Trim(); LifeSagaMemory.RecordWar( focus, warKey, cmd.label ?? "North", ended: false, provenance: "harness", other: other, note: string.IsNullOrEmpty(cmd.value) ? "North vs South" : cmd.value); if (other != null && other.isAlive()) { LifeSagaMemory.RecordWar( other, warKey, "South", ended: false, provenance: "harness", other: focus, note: string.IsNullOrEmpty(cmd.value) ? "North vs South" : cmd.value); } fact = LifeSagaMemory.StrongestUnresolved(EventFeedUtil.SafeId(focus)); } else if (kind == LifeSagaFactKind.PlotJoin) { string plotKey = string.IsNullOrEmpty(cmd.expect) ? "harness_plot" : cmd.expect.Trim(); LifeSagaMemory.RecordPlot( focus, plotKey, string.IsNullOrEmpty(cmd.value) ? "coup" : cmd.value, finished: false, provenance: "harness", other: other); if (other != null && other.isAlive()) { LifeSagaMemory.RecordPlot( other, plotKey, string.IsNullOrEmpty(cmd.value) ? "coup" : cmd.value, finished: false, provenance: "harness", other: focus); } fact = LifeSagaMemory.StrongestUnresolved(EventFeedUtil.SafeId(focus)); } else if (kind == LifeSagaFactKind.ParentChild) { if (other == null || !other.isAlive() || other == focus) { _cmdFail++; Emit(cmd, ok: false, detail: "parent_child_needs_partner"); break; } LifeSagaMemory.RecordParentChild(focus, other, provenance: "harness"); fact = null; var facts = LifeSagaMemory.FactsFor(EventFeedUtil.SafeId(focus)); for (int i = 0; i < facts.Count; i++) { if (facts[i] != null && facts[i].Kind == LifeSagaFactKind.ParentChild) { fact = facts[i]; break; } } } else { fact = LifeSagaMemory.Record( kind, focus, other, correlationKey: "harness:" + EventFeedUtil.SafeId(focus) + ":" + kind, strength: 80f, provenance: "harness", note: cmd.value ?? ""); } bool ok = fact != null || LifeSagaMemory.HasFacts(EventFeedUtil.SafeId(focus)); if (ok) _cmdOk++; else _cmdFail++; Emit(cmd, ok, detail: $"kind={kind} other={SafeName(other)} facts={LifeSagaMemory.FactsFor(EventFeedUtil.SafeId(focus)).Count}"); break; } case "saga_memory_resolve_war": { string warKey = string.IsNullOrEmpty(cmd.value) ? "harness_war" : cmd.value.Trim(); LifeSagaMemory.ResolveWarJoins(warKey); _cmdOk++; Emit(cmd, ok: true, detail: $"resolved_war={warKey}"); break; } case "saga_memory_resolve_plot": { string plotKey = string.IsNullOrEmpty(cmd.value) ? "harness_plot" : cmd.value.Trim(); LifeSagaMemory.ResolvePlotJoins(plotKey); _cmdOk++; Emit(cmd, ok: true, detail: $"resolved_plot={plotKey}"); break; } case "saga_role_rise_focus": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; string role = string.IsNullOrEmpty(cmd.value) ? "become_king" : cmd.value.Trim(); bool ok = LifeSagaRoster.HarnessSimulateRoleRise(focus, role); if (ok) _cmdOk++; else _cmdFail++; Emit(cmd, ok, detail: $"role={role} id={EventFeedUtil.SafeId(focus)}"); break; } case "saga_set_scores_focus": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; long id = EventFeedUtil.SafeId(focus); float standing = ParseFloat(cmd.value, 0.1f); float heat = ParseFloat(cmd.label, 0f); bool ok = id != 0 && LifeSagaRoster.HarnessSetScores(id, standing, heat); if (ok) _cmdOk++; else _cmdFail++; Emit(cmd, ok, detail: $"id={id} standing={standing} heat={heat}"); break; } case "soft_fill_quiet_arm": { StoryPlanner.NoteHardStoryQuiet(); _cmdOk++; Emit(cmd, ok: true, detail: $"softQuiet={StoryPlanner.SoftFillQuietActive}"); break; } case "saga_world_scan": { // Force a live admission scan even while harness Busy suppresses Refill discovery. LifeSagaRoster.HarnessWorldScan(); _cmdOk++; Emit(cmd, ok: true, detail: $"roster={LifeSagaRoster.Count}"); break; } case "saga_force_admit_six": { int admitted = 0; foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic()) { if (actor == null || !actor.isAlive()) { continue; } if (LifeSagaRoster.HarnessForceAdmit(actor)) { admitted++; } if (admitted >= 6) { break; } } bool ok = admitted >= 6 && LifeSagaRoster.Count == LifeSagaRoster.Cap; if (ok) _cmdOk++; else _cmdFail++; Emit( cmd, ok, detail: "admitted=" + admitted + " roster=" + LifeSagaRoster.Count + " cap=" + LifeSagaRoster.Cap); break; } case "hitch_probe": { _preserveSagaAfterBatch = true; bool on = ParseBool(cmd.value, defaultValue: true); IdleHitchProbe.SetEnabled(on); IdleHitchProbe.ResetStats(); _cmdOk++; Emit(cmd, ok: true, detail: $"hitch_probe={on}"); break; } case "narrative_scheduler_health_probe": { _preserveSagaAfterBatch = true; bool bounded = StoryScheduler.LastCandidateCount <= NarrativeStoryStore.SchedulerCopyLimit; bool responsive = StoryScheduler.LastTickMs < 50f; bool ok = bounded && responsive; if (ok) _cmdOk++; else _cmdFail++; Emit( cmd, ok, detail: "schedulerMs=" + StoryScheduler.LastTickMs.ToString("0.00") + " candidates=" + StoryScheduler.LastCandidateCount + "/" + NarrativeStoryStore.SchedulerCopyLimit + " graphEvents=" + NarrativeEventStore.Count + " graphThreads=" + NarrativeStoryStore.ThreadCount + " developments=" + NarrativeStoryStore.DevelopmentCount + " consequences=" + NarrativeStoryStore.ConsequenceCount + " characters=" + NarrativeStoryStore.CharacterCount + " compact=" + NarrativeStoryStore.CompactionCount + "/" + NarrativeStoryStore.LastCompactionMs.ToString("0.0") + "ms"); break; } case "load_save_slot": { int slot = Mathf.Clamp(ParseInt(cmd.value, 2), 1, 5); string detail; bool ok = TryLoadSaveSlot(slot, out detail); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: detail); break; } case "reflect_save_manager": { Type sm = AccessTools.TypeByName("SaveManager"); var sb = new StringBuilder(); sb.Append("type=").Append(sm != null ? sm.FullName : "null"); if (sm != null) { sb.Append(" methods="); foreach (var mi in sm.GetMethods( System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.DeclaredOnly)) { if (mi.Name.IndexOf("load", StringComparison.OrdinalIgnoreCase) < 0 && mi.Name.IndexOf("save", StringComparison.OrdinalIgnoreCase) < 0 && mi.Name.IndexOf("slot", StringComparison.OrdinalIgnoreCase) < 0) { continue; } sb.Append(mi.IsStatic ? "S:" : "I:"); sb.Append(mi.Name).Append('('); var ps = mi.GetParameters(); for (int i = 0; i < ps.Length; i++) { if (i > 0) { sb.Append(','); } sb.Append(ps[i].ParameterType.Name); } sb.Append(");"); } sb.Append(" fields="); foreach (var fi in sm.GetFields( System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic)) { sb.Append(fi.IsStatic ? "S:" : "I:"); sb.Append(fi.Name).Append(':').Append(fi.FieldType.Name).Append(';'); } } // Host owners of saveManager foreach (string hostName in new[] { "MapBox", "World", "Config", "SaveSlotManager" }) { Type host = AccessTools.TypeByName(hostName); if (host == null) { continue; } sb.Append(" host=").Append(hostName).Append('{'); foreach (var fi in host.GetFields( System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic)) { if (fi.Name.IndexOf("save", StringComparison.OrdinalIgnoreCase) < 0 && fi.Name.IndexOf("Save", StringComparison.Ordinal) < 0) { continue; } sb.Append(fi.IsStatic ? "S:" : "I:"); sb.Append(fi.Name).Append(':').Append(fi.FieldType.Name).Append(';'); } sb.Append('}'); } _cmdOk++; Emit(cmd, ok: true, detail: sb.ToString()); break; } case "interest_variety_note": { // Stamp the current (or pending needle in value) as the last shown variety arc. InterestCandidate c = InterestDirector.CurrentCandidate; string needle = (cmd.value ?? "").Trim(); if (!string.IsNullOrEmpty(needle)) { var pending = new System.Collections.Generic.List(64); InterestRegistry.CopyPending(pending); for (int i = 0; i < pending.Count; i++) { InterestCandidate p = pending[i]; if (p?.Key != null && p.Key.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0) { c = p; break; } } } if (c == null) { _cmdFail++; Emit(cmd, ok: false, detail: "no candidate to note"); break; } InterestVariety.NoteSelection(c, updateMix: false); InterestScoring.RecalcTotal(c); _cmdOk++; Emit( cmd, ok: true, detail: $"arc={InterestVariety.LastArcKey} streak={InterestVariety.ArcStreak} key={c.Key}"); break; } case "interest_variety_push_arc": { // value = arc key (or short "combat:duel"), label/count = times string arc = (cmd.value ?? "").Trim(); if (!string.IsNullOrEmpty(arc) && !arc.StartsWith("arc:", StringComparison.OrdinalIgnoreCase)) { arc = "arc:" + arc; } int times = cmd.count > 0 ? cmd.count : ParseInt(cmd.label, 8); if (string.IsNullOrEmpty(arc)) { _cmdFail++; Emit(cmd, ok: false, detail: "no arc"); break; } InterestVariety.HarnessPushArc(arc, times); _cmdOk++; Emit( cmd, ok: true, detail: $"pushed={arc} x{times} window={InterestVariety.ArcWindowCount}"); break; } case "scoring_reload": { bool ok = InterestScoringConfig.Reload(); bool catalogOk = EventCatalogConfig.Reload(); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok: ok, detail: ok ? $"loaded v={InterestScoringConfig.LoadedVersion} path={InterestScoringConfig.LoadedPath} " + $"eventCatalog={catalogOk} decisions={EventCatalogConfig.DecisionCount}" : "scoring_reload_failed"); break; } case "event_catalog_reload": { bool ok = EventCatalogConfig.Reload(); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok: ok, detail: ok ? $"loaded v={EventCatalogConfig.LoadedVersion} path={EventCatalogConfig.LoadedPath}" : "event_catalog_reload_failed"); break; } case "interest_variety_seed": { // value = "events:chars" e.g. "2:10" string raw = (cmd.value ?? "").Trim(); int events = 0; int chars = 0; int colon = raw.IndexOf(':'); if (colon >= 0) { int.TryParse(raw.Substring(0, colon), NumberStyles.Integer, CultureInfo.InvariantCulture, out events); int.TryParse(raw.Substring(colon + 1), NumberStyles.Integer, CultureInfo.InvariantCulture, out chars); } else { int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out events); } InterestVariety.HarnessSeedMix(events, chars); _cmdOk++; Emit( cmd, ok: true, detail: $"seeded events={events} chars={chars} share={InterestVariety.EventShare():0.##} samples={InterestVariety.MixSampleCount}"); break; } case "interest_end_session": InterestDirector.HarnessEndCurrent(cmd.value ?? "harness"); _cmdOk++; Emit( cmd, ok: true, detail: $"ended key={InterestDirector.CurrentKey} interrupted={InterestDirector.InterruptedKey}"); break; case "interest_release_force": InterestDirector.HarnessReleaseForceActive(); _cmdOk++; Emit(cmd, ok: true, detail: $"released key={InterestDirector.CurrentKey} active={InterestDirector.CurrentIsActive}"); break; case "combat_disengage_pair": { // Clear fight signals without force-cold - proves natural CombatStillActive release. InterestCandidate scene = InterestDirector.CurrentCandidate; Actor a = scene?.FollowUnit; Actor b = scene?.RelatedUnit; for (int i = 0; i < 4; i++) { ClearAttackTarget(a); ClearAttackTarget(b); TryClearCombatTask(a); TryClearCombatTask(b); } // Drop LastSeenAt so hysteresis cannot bridge a cleared scrap; live fighting // still refreshes hot via IsCombatParticipant on the next tick. if (scene != null) { scene.LastSeenAt = Time.unscaledTime - 60f; } bool aFight = LiveEnsemble.IsCombatParticipant(a); bool bFight = LiveEnsemble.IsCombatParticipant(b); _cmdOk++; Emit( cmd, ok: true, detail: $"disengaged a={SafeName(a)}:{aFight} b={SafeName(b)}:{bFight} tip='{CameraDirector.LastWatchLabel}' active={InterestDirector.CurrentIsActive}"); break; } case "combat_spark_assets": { // Wire two species near the camera into a scrap without stamping the current scene. try { string sideA = string.IsNullOrEmpty(cmd.asset) ? "bear" : cmd.asset.Trim(); string sideB = string.IsNullOrEmpty(cmd.value) ? "crocodile" : cmd.value.Trim(); Vector3 origin = CameraPos(); Actor a = WorldActivityScanner.FindNearestAliveUnit(origin, 80f, sideA); Actor b = WorldActivityScanner.FindNearestAliveUnit(origin, 80f, sideB); if (b != null && a != null && b == a) { b = FindNearestAliveAsset(sideB, preferUnfocused: true, otherThan: a); } int wired = 0; if (TrySetAttackTarget(a, b)) { wired++; } if (TrySetAttackTarget(b, a)) { wired++; } int stickyA = 0; int stickyB = 0; InterestCandidate cur = InterestDirector.CurrentCandidate; if (cur != null) { stickyA = cur.CombatSideACount; stickyB = cur.CombatSideBCount; } bool ok = wired > 0; if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"sparked={wired} a={SafeName(a)}:{sideA} b={SafeName(b)}:{sideB} sceneSticky={stickyA}:{stickyB}"); } catch (Exception ex) { _cmdFail++; Emit(cmd, ok: false, detail: "exception: " + ex.GetType().Name + ": " + ex.Message); } break; } case "interest_mark_combat_cold": { // Drop attack/combat signals so live path cannot refresh hot; force-cold gates the hold. InterestCandidate scene = InterestDirector.CurrentCandidate; if (scene != null) { ClearAttackTarget(scene.FollowUnit); ClearAttackTarget(scene.RelatedUnit); TryClearCombatTask(scene.FollowUnit); TryClearCombatTask(scene.RelatedUnit); } InterestDirector.HarnessMarkCombatCold(); _cmdOk++; Emit( cmd, ok: true, detail: $"combat_cold key={InterestDirector.CurrentKey} tip='{CameraDirector.LastWatchLabel}' active={InterestDirector.CurrentIsActive}"); break; } case "interest_mark_sticky_cold": case "story_mark_climax_cold": { InterestCandidate scene = InterestDirector.CurrentCandidate; if (scene != null) { ClearAttackTarget(scene.FollowUnit); ClearAttackTarget(scene.RelatedUnit); TryClearCombatTask(scene.FollowUnit); TryClearCombatTask(scene.RelatedUnit); } InterestDirector.HarnessMarkStickySceneCold(); _cmdOk++; Emit( cmd, ok: true, detail: $"sticky_cold key={InterestDirector.CurrentKey} tip='{CameraDirector.LastWatchLabel}' active={InterestDirector.CurrentIsActive}"); break; } case "interest_mark_inactive": { float ago = ParseFloat(cmd.value, 1f); InterestDirector.HarnessMarkInactive(ago); // Rebuild dossier so orange reason clears under quiet_grace / inactive dwell. if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null) { WatchCaption.SetFromActor(MoveCamera._focus_unit); } _cmdOk++; Emit( cmd, ok: true, detail: $"inactive_for={ago:0.##}s active={InterestDirector.CurrentIsActive} key={InterestDirector.CurrentKey} reason='{WatchCaption.Current?.ReasonLine}'"); break; } case "interest_drop_follow": { // Null Follow only - keep SubjectId/Related for story cast recovery tests. InterestCandidate scene = InterestDirector.CurrentCandidate; if (scene == null) { _cmdFail++; Emit(cmd, ok: false, detail: "no_scene"); break; } long keepSubject = scene.SubjectId != 0 ? scene.SubjectId : EventFeedUtil.SafeId(scene.FollowUnit); scene.FollowUnit = null; if (scene.SubjectId == 0 && keepSubject != 0) { scene.SubjectId = keepSubject; } _cmdOk++; Emit(cmd, ok: true, detail: $"dropped_follow subjectId={scene.SubjectId} relatedId={scene.RelatedId}"); break; } case "interest_clear_follow": { Actor related = ResolveUnit(string.IsNullOrEmpty(cmd.asset) || cmd.asset == "auto" ? null : cmd.asset); if (related == null) { related = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f); } // Prefer a different living unit than the current follow. Actor follow = InterestDirector.CurrentCandidate?.FollowUnit; if (related != null && follow != null && related == follow) { foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic()) { if (actor != null && actor != follow) { try { if (actor.isAlive()) { related = actor; break; } } catch { // continue } } } } bool ok = InterestDirector.HarnessClearFollowForHandoff(related); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: $"cleared related={SafeName(related)} key={InterestDirector.CurrentKey}"); break; } case "camera_clear_focus": { // Simulate vanilla clearing follow mid-idle (death / spectator desync). CameraDirector.ClearFollow(); bool cleared = !MoveCamera.hasFocusUnit() || MoveCamera._focus_unit == null || !MoveCamera._focus_unit.isAlive(); if (cleared) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok: cleared, detail: $"cleared={cleared} idle={SpectatorMode.Active}"); break; } case "interest_expire_pending": { string needle = cmd.value ?? ""; InterestRegistry.ForceExpireContaining(needle, Time.unscaledTime); _cmdOk++; Emit(cmd, ok: true, detail: $"expired needle='{needle}' pending={InterestRegistry.PendingCount}"); break; } case "interest_feeds_tick": { int drained = InterestFeeds.HarnessDrainOnce(); _cmdOk++; Emit( cmd, ok: true, detail: $"drained={drained} cursor={InterestFeeds.HappinessCursor} pending={InterestRegistry.PendingCount}"); break; } case "interest_peek_next": { InterestCandidate peek = InterestDirector.HarnessPeekNext(); _cmdOk++; Emit( cmd, ok: true, detail: peek == null ? "peek=null" : $"peek={peek.Key} lead={peek.LeadKind} score={peek.TotalScore:0.#} label={peek.Label}"); break; } case "interest_civic_boost": { string effectId = !string.IsNullOrEmpty(cmd.value) ? cmd.value.Trim() : "just_started_war"; string kingdom = !string.IsNullOrEmpty(cmd.label) ? cmd.label : "harness"; float amount = ParseFloat(cmd.expect, 40f); InterestFeeds.HarnessStampCivicBoost(kingdom, effectId, amount); _cmdOk++; Emit(cmd, ok: true, detail: $"civic kingdom={kingdom} effect={effectId} amt={amount:0.#}"); break; } case "happiness_suppress": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; if (focus == null && InterestDirector.CurrentCandidate != null) { focus = InterestDirector.CurrentCandidate.FollowUnit; } if (focus == null) { focus = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f); } string effectId = !string.IsNullOrEmpty(cmd.value) ? cmd.value.Trim() : "death_child"; if (focus == null) { _cmdFail++; Emit(cmd, ok: false, detail: "no unit for happiness_suppress"); break; } HappinessEventRouter.PublishSuppressed(focus, effectId, HappinessSourceHook.Harness); _cmdOk++; Emit( cmd, ok: true, detail: $"suppressed={effectId} unit={SafeName(focus)} seq={HappinessEventRouter.CurrentSequence}"); break; } case "happiness_remember_only": { // Remember a Signal for DrainSince without immediate Ingest/Register. Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; if (focus == null && InterestDirector.CurrentCandidate != null) { focus = InterestDirector.CurrentCandidate.FollowUnit; } if (focus == null) { focus = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f); } string effectId = !string.IsNullOrEmpty(cmd.value) ? cmd.value.Trim() : "death_lover"; if (focus == null || !focus.isAlive()) { _cmdFail++; Emit(cmd, ok: false, detail: "no living unit for happiness_remember_only"); break; } HappinessOccurrence published = HappinessEventRouter.PublishApplied( focus, effectId, HappinessGameApi.ResolveAmount(effectId), HappinessSourceHook.Harness, related: null, role: HappinessRelationRole.None, forceRing: true); bool ok = published != null; if (ok) { InterestFeeds.HarnessEnsureCursorBefore(published.Sequence); _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"remembered={effectId} seq={HappinessEventRouter.CurrentSequence} cursor={InterestFeeds.HappinessCursor} unit={SafeName(focus)}"); break; } case "set_setting": DoSetSetting(cmd); break; case "assert": DoAssert(cmd); break; case "snapshot": if (WantsPreserveSaga(cmd)) { _preserveSagaAfterBatch = true; } DoSnapshot(cmd); break; case "soak_probe": case "saga_soak_probe": DoSoakProbe(cmd); break; case "scenario": ExpandScenario(cmd); return; case "reset_counters": { StateProbe.ResetCounters(); string mode = (cmd.value ?? cmd.expect ?? "").Trim().ToLowerInvariant(); if (mode == "all" || mode == "harness") { _assertPass = 0; _assertFail = 0; _cmdOk = 0; _cmdFail = 0; } _cmdOk++; Emit(cmd, ok: true, detail: mode == "all" || mode == "harness" ? "counters_reset_all" : "stateprobe_reset"); break; } default: _cmdFail++; Emit(cmd, ok: false, detail: "unknown action: " + action); break; } if (Queue.Count == 0) { // Free AFK after harness must not inherit a leftover crisis closer tip. StoryPlanner.PurgeCloserLeftovers(); bool preserve = _preserveSagaAfterBatch; _preserveSagaAfterBatch = false; if (!preserve) { LifeSagaRoster.Clear(); LifeSagaMemory.Clear(); NarrativeStoryStore.Clear(); } string status = _assertFail == 0 && _cmdFail == 0 ? "PASS" : "FAIL"; string wipe = preserve ? "saga_preserved" : "saga_wiped"; WriteLastResult(status, $"ok={_cmdOk} fail={_cmdFail} assert_pass={_assertPass} assert_fail={_assertFail} {wipe}"); LogService.LogInfo( $"[IdleSpectator][HARNESS] batch done status={status} ok={_cmdOk} fail={_cmdFail} assert_pass={_assertPass} assert_fail={_assertFail} {wipe}"); } } private static void ExpandScenario(HarnessCommand cmd) { string name = (cmd.value ?? "").Trim().ToLowerInvariant(); List steps = HarnessScenarios.Build(name); if (steps == null || steps.Count == 0) { _cmdFail++; Emit(cmd, ok: false, detail: "unknown scenario: " + name); WriteLastResult("FAIL", "unknown scenario: " + name); return; } _lastBatchId = string.IsNullOrEmpty(cmd.id) ? name : cmd.id; // Scenario batches always wipe at end - never inherit a prior soak preserve flag. _preserveSagaAfterBatch = false; bool keepStats = (cmd.expect ?? "").Trim().Equals("keep", System.StringComparison.OrdinalIgnoreCase); if (!keepStats) { _assertPass = 0; _assertFail = 0; _cmdOk = 0; _cmdFail = 0; StateProbe.ResetCounters(); } // Re-queue scenario steps ahead of anything else already queued. List rest = new List(Queue); Queue.Clear(); foreach (HarnessCommand step in steps) { Queue.Enqueue(step); } foreach (HarnessCommand step in rest) { Queue.Enqueue(step); } _cmdOk++; Emit(cmd, ok: true, detail: $"expanded={name} steps={steps.Count}"); LogService.LogInfo($"[IdleSpectator][HARNESS] scenario={name} steps={steps.Count}"); } private static void DoPickUnit(HarnessCommand cmd) { if (!WorldReady()) { if (RetryFront(cmd, "world_not_ready")) { return; } _cmdFail++; Emit(cmd, ok: false, detail: "world_not_ready"); return; } string assetId = string.IsNullOrEmpty(cmd.asset) || cmd.asset == "auto" ? null : cmd.asset; string mode = (cmd.value ?? "").Trim().ToLowerInvariant(); bool wantOther = mode.IndexOf("other", StringComparison.Ordinal) >= 0; bool preferOffRoster = mode.IndexOf("off_roster", StringComparison.Ordinal) >= 0 || mode.IndexOf("other", StringComparison.Ordinal) >= 0; Actor exclude = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : _lastSpawned; Actor unit = null; if (wantOther) { unit = FindOtherAliveUnitPreferOffRoster(exclude, assetId, preferOffRoster); } else { unit = ResolveUnit(assetId); if (unit == null || !unit.isAlive()) { // Ignore stale harness spawn handle; scan the live world. _lastSpawned = null; unit = string.IsNullOrEmpty(assetId) ? WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 5000f) : WorldActivityScanner.FindAliveByAssetId(assetId, CameraPos(), 5000f); } } if (unit == null || !unit.isAlive()) { if (RetryFront(cmd, "no_unit")) { return; } _cmdFail++; Emit(cmd, ok: false, detail: "no living unit in world"); return; } _lastSpawned = unit; _lastSpawnedAssetId = unit.asset != null ? unit.asset.id : ""; CameraDirector.FocusUnit(unit); _cmdOk++; Emit(cmd, ok: true, detail: $"picked={SafeName(unit)} asset={_lastSpawnedAssetId} other={wantOther}"); } private static void DoSpawn(HarnessCommand cmd) { if (!WorldReady()) { if (RetryFront(cmd, "world_not_ready")) { return; } _cmdFail++; Emit(cmd, ok: false, detail: "world_not_ready"); return; } string assetId = string.IsNullOrEmpty(cmd.asset) ? cmd.value : cmd.asset; if (string.IsNullOrEmpty(assetId) || assetId == "auto") { assetId = PickSpawnAssetId(); if (string.IsNullOrEmpty(assetId)) { _cmdFail++; Emit(cmd, ok: false, detail: "no spawnable asset"); return; } } if (AssetManager.actor_library?.get(assetId) == null) { _cmdFail++; Emit(cmd, ok: false, detail: "unknown asset: " + assetId); return; } WorldTile tile = null; try { tile = TileNearCamera(); // Prefer spawning next to an existing living unit so terrain is valid. Actor anchor = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 5000f); if (anchor != null) { WorldTile anchorTile = World.world.GetTile( Mathf.RoundToInt(anchor.current_position.x), Mathf.RoundToInt(anchor.current_position.y)); if (anchorTile != null) { tile = anchorTile; } } } catch (Exception) { tile = null; } if (tile == null) { if (RetryFront(cmd, "no_tile")) { return; } _cmdFail++; Emit(cmd, ok: false, detail: "no tile near camera"); return; } int count = Math.Max(1, cmd.count); int spawned = 0; Actor last = null; Exception lastEx = null; for (int i = 0; i < count; i++) { try { Actor actor = World.world.units.spawnNewUnit(assetId, tile, pSpawnSound: false, pMiracleSpawn: true); if (actor != null && actor.isAlive()) { spawned++; last = actor; } } catch (Exception ex) { lastEx = ex; } } if (spawned <= 0 || last == null || !last.isAlive()) { if (RetryFront(cmd, lastEx != null ? "spawn_ex:" + lastEx.GetType().Name : "spawn_zero")) { return; } _cmdFail++; Emit( cmd, ok: false, detail: lastEx != null ? $"spawn_exception asset={assetId} msg={lastEx.Message}" : $"spawned=0/{count} asset={assetId}"); return; } _lastSpawned = last; _lastSpawnedAssetId = last.asset != null ? last.asset.id : assetId; try { CameraDirector.FocusUnit(last); } catch (Exception) { // Spawn succeeded; focus is best-effort. } _cmdOk++; Emit(cmd, ok: true, detail: $"spawned={spawned}/{count} asset={_lastSpawnedAssetId} last={SafeName(last)}"); } private static void DoFocus(HarnessCommand cmd) { if (!WorldReady()) { if (RetryFront(cmd, "world_not_ready")) { return; } _cmdFail++; Emit(cmd, ok: false, detail: "world_not_ready"); return; } string assetId = string.IsNullOrEmpty(cmd.asset) ? cmd.value : cmd.asset; if (assetId == "nearest" || assetId == "auto") { assetId = assetId == "auto" ? _lastSpawnedAssetId : null; } Actor unit = ResolveUnit(assetId); if (unit == null || !unit.isAlive()) { if (RetryFront(cmd, "no_unit")) { return; } _cmdFail++; Emit(cmd, ok: false, detail: "no unit to focus"); return; } CameraDirector.FocusUnit(unit); try { _activitySubjectId = unit.getID(); } catch { _activitySubjectId = 0; } _cmdOk++; Emit(cmd, ok: true, detail: $"unit={SafeName(unit)} asset={(unit.asset != null ? unit.asset.id : "?")}"); } private static void DoWatch(HarnessCommand cmd) { if (!WorldReady()) { if (RetryFront(cmd, "world_not_ready")) { return; } _cmdFail++; Emit(cmd, ok: false, detail: "world_not_ready"); return; } string assetId = cmd.asset; if (assetId == "auto") { assetId = _lastSpawnedAssetId; } Actor follow = ResolveUnit(string.IsNullOrEmpty(assetId) ? null : assetId); if (follow == null) { if (RetryFront(cmd, string.IsNullOrEmpty(assetId) ? "no_unit" : "no_asset_unit")) { return; } _cmdFail++; Emit(cmd, ok: false, detail: string.IsNullOrEmpty(assetId) ? "no unit for watch" : "no unit for asset: " + assetId); return; } float tierEvt = EventStrengthFromTierHint(cmd.tier, 40f); InterestLeadKind tierLead = LeadFromTierHint(cmd.tier, InterestLeadKind.CharacterLed); string resolvedAsset = follow.asset != null ? follow.asset.id : assetId; string label = string.IsNullOrEmpty(cmd.label) ? $"Harness: {resolvedAsset}" : cmd.label.Replace("{asset}", resolvedAsset ?? ""); if (label.Contains("{asset}")) { label = label.Replace("{asset}", resolvedAsset ?? ""); } InterestEvent interest = new InterestEvent { Score = tierEvt, Position = follow.current_position, FollowUnit = follow, Label = label, CreatedAt = Time.unscaledTime, AssetId = follow.asset != null ? follow.asset.id : assetId }; CameraDirector.Watch(interest); _cmdOk++; Emit(cmd, ok: true, detail: $"tip={label} unit={SafeName(follow)}"); } private static void DoTriggerInterest(HarnessCommand cmd) { if (!WorldReady()) { _cmdFail++; Emit(cmd, ok: false, detail: "world_not_ready"); return; } string assetId = cmd.asset; Actor follow = null; if (string.IsNullOrEmpty(assetId) && MoveCamera.hasFocusUnit()) { Actor focused = MoveCamera._focus_unit; if (focused != null && focused.isAlive()) { follow = focused; } } if (!string.IsNullOrEmpty(assetId)) { follow = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f, assetId); } if (follow == null) { follow = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f); } if (follow == null) { _cmdFail++; Emit(cmd, ok: false, detail: "no unit for interest"); return; } float tierEvt = EventStrengthFromTierHint(cmd.tier, 40f); InterestLeadKind tierLead = LeadFromTierHint(cmd.tier, InterestLeadKind.CharacterLed); ParseInterestInjectOptions( cmd.value, out bool forceIgnored, out InterestLeadKind? leadOpt, out float eventStrength, out float charSigIgnored, out float maxWatchIgnored, out float ttlIgnored, out bool resumableIgnored, out int participantsIgnored, out int notablesIgnored); _ = forceIgnored; _ = charSigIgnored; _ = maxWatchIgnored; _ = ttlIgnored; _ = resumableIgnored; _ = participantsIgnored; _ = notablesIgnored; InterestLeadKind lead = leadOpt ?? tierLead; if (eventStrength >= 0f) { tierEvt = eventStrength; } string tipLabel = string.IsNullOrEmpty(cmd.label) ? $"Harness {cmd.tier ?? "scene"}" : cmd.label.Replace("{asset}", follow.asset != null ? follow.asset.id : (assetId ?? "")); InterestEvent interest = new InterestEvent { Score = tierEvt, Position = follow.current_position, FollowUnit = follow, Label = tipLabel, CreatedAt = Time.unscaledTime, AssetId = follow.asset != null ? follow.asset.id : assetId }; bool eventLedNotice = lead == InterestLeadKind.EventLed && tierEvt >= InterestScoringConfig.W.noticeScoreMin; InterestCandidate registered = InterestFeeds.RegisterHarness( follow, interest.Label, keySuffix: (interest.Label ?? "scene").Replace(" ", "_"), lead: lead, completion: InterestCompletionKind.FixedDwell, forceActive: false, eventStrength: tierEvt, maxWatch: tierEvt >= 95f ? 40f : 25f); if (registered == null) { InterestCollector.EnqueueDirect(interest); } else if (eventLedNotice) { // Only force the camera when score-margin cut-in would allow it (matches director policy). InterestScoring.ScoreCheap(registered); float cur = InterestDirector.CurrentScore; bool noCurrent = InterestDirector.CurrentCandidate == null; bool marginCut = registered.TotalScore >= cur + InterestScoringConfig.W.cutInMargin; if (noCurrent || marginCut) { InterestDirector.HarnessForceSession(registered); CameraDirector.Watch(registered.ToInterestEvent()); } } _cmdOk++; Emit( cmd, ok: true, detail: $"triggered label={tipLabel} evt={tierEvt:0.#} forced={InterestDirector.CurrentKey != null && InterestDirector.CurrentKey.IndexOf((interest.Label ?? "").Replace(" ", "_"), System.StringComparison.OrdinalIgnoreCase) >= 0}"); } /// /// Feed a live WorldLog asset id through InterestFeeds as if WorldLog fired (coverage of authored catalog). /// private static void DoWorldLogFeed(HarnessCommand cmd) { if (!WorldReady()) { _cmdFail++; Emit(cmd, ok: false, detail: "world_not_ready"); return; } string assetId = string.IsNullOrEmpty(cmd.asset) ? (cmd.value ?? "") : cmd.asset; if (string.IsNullOrEmpty(assetId) || assetId == "auto") { assetId = "disaster_tornado"; } if (!EventCatalog.WorldLog.HasAuthored(assetId)) { _cmdFail++; Emit(cmd, ok: false, detail: "not_authored:" + assetId); return; } WorldLogEventEntry entry = EventCatalog.WorldLog.GetOrFallback(assetId); Actor follow = ResolveUnit(null) ?? WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f); if (follow == null && entry.CreatesInterest) { _cmdFail++; Emit(cmd, ok: false, detail: "no unit for world_log_feed"); return; } if (!entry.CreatesInterest) { _cmdOk++; Emit(cmd, ok: true, detail: $"skipped_no_interest id={assetId}"); return; } string kingdom = follow.kingdom != null ? (follow.kingdom.name ?? "Alpha") : "Alpha"; string key = "worldlog:" + assetId + ":" + kingdom; var candidate = new InterestCandidate { Key = key, LeadKind = InterestLeadKind.EventLed, Category = entry.Category, Source = "worldlog", EventStrength = entry.EventStrength, VisualConfidence = 0.7f, Position = follow.current_position, FollowUnit = follow, SubjectId = follow.getID(), Label = entry.MakeLabel(kingdom, "Beta"), AssetId = assetId, KingdomKey = kingdom, SpeciesId = follow.asset != null ? follow.asset.id : "", CreatedAt = Time.unscaledTime, LastSeenAt = Time.unscaledTime, ExpiresAt = Time.unscaledTime + 40f, MinWatch = entry.MinWatch, MaxWatch = entry.MaxWatch, Completion = InterestCompletionKind.FixedDwell }; InterestScoring.ScoreCheap(candidate); EventFeedUtil.RegisterCandidate(candidate); _cmdOk++; Emit(cmd, ok: true, detail: $"fed id={assetId} key={key} evt={entry.EventStrength:0.#}"); } /// /// Proves unpresentable Labels are rejected at Register (selection = presentation). /// private static void DoPresentabilityProbe(HarnessCommand cmd) { if (!WorldReady()) { _cmdFail++; Emit(cmd, ok: false, detail: "world_not_ready"); return; } Actor unit = ResolveUnit(string.IsNullOrEmpty(cmd.asset) || cmd.asset == "auto" ? null : cmd.asset) ?? WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f); if (unit == null) { _cmdFail++; Emit(cmd, ok: false, detail: "no unit for presentability_probe"); return; } InterestDropLog.Clear(); string name = EventFeedUtil.SafeName(unit); string mode = (cmd.value ?? "fall").Trim().ToLowerInvariant(); string label; if (mode == "good" || mode == "presentable") { label = string.IsNullOrEmpty(cmd.label) ? EventReason.Fight(unit, null) : cmd.label; if (!label.StartsWith(name, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(name)) { label = EventPresentability.EnsureSubjectLedLabel(unit, label); } } else { // Article-led fall shape that dossier blanks via identity_filter. label = string.IsNullOrEmpty(cmd.label) ? ("A Maple Tree falls near " + (string.IsNullOrEmpty(name) ? "someone" : name)) : cmd.label; } string key = "probe:presentability:" + EventFeedUtil.SafeId(unit) + ":" + Time.unscaledTime.ToString("0.###"); InterestCandidate registered = EventFeedUtil.Register( key, "probe", "Spectacle", label, "presentability_probe", 88f, unit.current_position, unit); bool expectReject = mode != "good" && mode != "presentable"; bool ok; string detail; if (expectReject) { ok = registered == null && InterestDropLog.RecentContains("unpresentable"); detail = $"reject label='{label}' registered={registered != null} drops='{InterestDropLog.FormatRecent(4)}'"; } else { ok = registered != null && EventPresentability.WouldShow(registered); detail = $"accept label='{label}' registered={registered != null} key={(registered != null ? registered.Key : "")}"; } if (ok) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok: ok, detail: detail); } /// /// Injects a Signal from a domain catalog while the harness is Busy /// (domain feeds themselves early-out on Busy, so register here). /// asset = domain, value = event/asset id. /// private static void DoDomainFeed(HarnessCommand cmd) { if (!WorldReady()) { _cmdFail++; Emit(cmd, ok: false, detail: "world_not_ready"); return; } string domain = (cmd.asset ?? "").Trim().ToLowerInvariant(); string id = (cmd.value ?? cmd.expect ?? "").Trim(); Actor unit = ResolveUnit(null) ?? WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f); if (unit == null && domain != "era") { _cmdFail++; Emit(cmd, ok: false, detail: "no unit for domain_feed"); return; } try { DiscreteEventEntry entry; string key; string label; float strength; string category; string source; switch (domain) { case "relationship": case "rel": if (string.IsNullOrEmpty(id)) { id = "set_lover"; } entry = EventCatalog.Relationship.GetOrFallback(id); Actor related = ActorRelation.ResolvePackRelated(unit) ?? WorldActivityScanner.FindNearestAliveUnit(unit.current_position, 40f); if (related == unit) { related = null; } key = "rel:" + entry.Id + ":" + EventFeedUtil.SafeId(unit) + ":" + EventFeedUtil.SafeId(related); label = entry.MakeLabel( EventFeedUtil.SafeName(unit), EventFeedUtil.SafeName(related)); strength = entry.EventStrength; category = entry.Category; source = "relationship"; InterestCandidate relReg = EventFeedUtil.Register( key, source, category, label, id, strength, unit.current_position, unit, related: related); if (relReg == null) { _cmdFail++; Emit(cmd, ok: false, detail: "register_failed domain=relationship id=" + id); return; } _cmdOk++; Emit(cmd, ok: true, detail: $"domain=relationship id={id} key={key}"); return; case "plot": if (string.IsNullOrEmpty(id)) { id = "new_war"; } entry = EventCatalog.Plot.GetOrFallback(id); key = "plot:" + entry.Id + ":new:" + EventFeedUtil.SafeId(unit); label = EventReason.Plot(unit, entry.LabelTemplate, "new", entry.Id); strength = entry.EventStrength; category = entry.Category; source = "plot"; break; case "era": if (string.IsNullOrEmpty(id)) { id = "age_chaos"; } entry = EventCatalog.Era.GetOrFallback(id); key = "era:" + entry.Id; label = entry.MakeLabel("", ""); strength = entry.EventStrength; category = entry.Category; source = "era"; Vector3 eraPos = unit != null ? unit.current_position : (Camera.main != null ? new Vector3(Camera.main.transform.position.x, Camera.main.transform.position.y, 0f) : new Vector3(1f, 1f, 0f)); InterestCandidate eraReg = EventFeedUtil.Register( key, source, category, label, id, strength, eraPos, follow: unit, locationOnly: unit == null); if (eraReg == null) { _cmdFail++; Emit(cmd, ok: false, detail: "register_failed domain=era id=" + id); return; } _cmdOk++; Emit(cmd, ok: true, detail: $"domain=era id={id} key={key}"); return; case "book": if (string.IsNullOrEmpty(id)) { id = "history_book"; } entry = EventCatalog.Book.GetOrFallback(id); key = "book:" + entry.Id + ":new:" + EventFeedUtil.SafeId(unit); label = entry.MakeLabel(EventFeedUtil.SafeName(unit), "new"); strength = entry.EventStrength; category = entry.Category; source = "book"; break; case "trait": if (string.IsNullOrEmpty(id)) { id = "immortal"; } entry = EventCatalog.Trait.GetOrFallback(id); key = "trait:" + entry.Id + ":gains:" + EventFeedUtil.SafeId(unit); label = EventReason.Trait(unit, entry.Id, gained: true); strength = entry.EventStrength; category = entry.Category; source = "trait"; break; case "meta": if (string.IsNullOrEmpty(id)) { id = "new_city"; } key = "meta:" + id + ":" + EventFeedUtil.SafeId(unit); label = EventReason.MetaNew(unit, id); strength = 74f; category = "Politics"; source = "meta"; break; case "boat": id = "boat_unload"; key = "boat:boat_unload:" + EventFeedUtil.SafeId(unit); label = EventReason.Boat(unit, "unload"); strength = 72f; category = "Travel"; source = "boat"; break; case "building": id = "building_complete"; key = "building:complete:harness:" + EventFeedUtil.SafeId(unit); label = EventReason.BuildingComplete(unit, "temple_human", spectacle: true); strength = 86f; category = "Settlement"; source = "building"; break; case "spell": if (string.IsNullOrEmpty(id)) { id = "summon_lightning"; } { DiscreteEventEntry spellEntry = EventCatalog.Spell.GetOrFallback(id); key = "spell:" + spellEntry.Id + ":" + EventFeedUtil.SafeId(unit); // Match live DeferredInterestFeed (EventReason.Library), not raw template {id}. label = EventReason.Library(unit, "spell", spellEntry.Id); strength = spellEntry.EventStrength; category = spellEntry.Category; source = "spell"; id = spellEntry.Id; } break; case "item": if (string.IsNullOrEmpty(id)) { id = "sword"; } { DiscreteEventEntry itemEntry = EventCatalog.Item.GetOrFallback(id); key = "item:" + itemEntry.Id + ":" + EventFeedUtil.SafeId(unit); label = itemEntry.MakeLabel(EventFeedUtil.SafeName(unit), ""); strength = itemEntry.EventStrength; category = itemEntry.Category; source = "item"; id = itemEntry.Id; } break; case "decision": if (string.IsNullOrEmpty(id)) { id = "declare_war"; } { DiscreteEventEntry decEntry = EventCatalog.Decision.GetOrFallback(id); key = "decision:" + decEntry.Id + ":" + EventFeedUtil.SafeId(unit); label = decEntry.MakeLabel(EventFeedUtil.SafeName(unit), ""); strength = decEntry.EventStrength; category = decEntry.Category; source = "decision"; id = decEntry.Id; } break; case "power": if (string.IsNullOrEmpty(id)) { id = "lightning"; } { DiscreteEventEntry powEntry = EventCatalog.Power.GetOrFallback(id); key = "power:" + powEntry.Id + ":" + EventFeedUtil.SafeId(unit); label = powEntry.MakeLabel(EventFeedUtil.SafeName(unit), ""); strength = powEntry.EventStrength; category = powEntry.Category; source = "power"; id = powEntry.Id; } break; case "subspecies_trait": case "subspecies": if (string.IsNullOrEmpty(id)) { id = "biome_trait"; } { DiscreteEventEntry stEntry = EventCatalog.SubspeciesTrait.GetOrFallback(id); key = "subspecies_trait:" + stEntry.Id + ":" + EventFeedUtil.SafeId(unit); label = stEntry.MakeLabel(EventFeedUtil.SafeName(unit), ""); strength = stEntry.EventStrength; category = stEntry.Category; source = "subspecies_trait"; id = stEntry.Id; } break; case "gene": if (string.IsNullOrEmpty(id)) { id = "warfare_1"; } { DiscreteEventEntry geneEntry = EventCatalog.Gene.GetOrFallback(id); key = "gene:" + geneEntry.Id + ":" + EventFeedUtil.SafeId(unit); label = geneEntry.MakeLabel(EventFeedUtil.SafeName(unit), ""); strength = geneEntry.EventStrength; category = geneEntry.Category; source = "gene"; id = geneEntry.Id; } break; default: _cmdFail++; Emit(cmd, ok: false, detail: "unknown domain=" + domain); return; } InterestCandidate registered = EventFeedUtil.Register( key, source, category, label, id, strength, unit.current_position, unit); if (registered == null) { _cmdFail++; Emit(cmd, ok: false, detail: "register_failed domain=" + domain + " id=" + id); return; } _cmdOk++; Emit(cmd, ok: true, detail: $"domain={domain} id={id} key={key}"); } catch (System.Exception ex) { _cmdFail++; Emit(cmd, ok: false, detail: "domain_feed error: " + ex.Message); } } private static void DoInterestInject(HarnessCommand cmd) { if (!WorldReady()) { _cmdFail++; Emit(cmd, ok: false, detail: "world_not_ready"); return; } Actor follow = ResolveUnit(string.IsNullOrEmpty(cmd.asset) || cmd.asset == "auto" ? null : cmd.asset); if (follow == null) { follow = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f); } if (follow == null) { _cmdFail++; Emit(cmd, ok: false, detail: "no unit for interest_inject"); return; } // pick=other: use a different living unit (same asset when possible) for ledger/near-tie tests. if (!string.IsNullOrEmpty(cmd.value) && cmd.value.IndexOf("pick=other", StringComparison.OrdinalIgnoreCase) >= 0) { Actor other = FindOtherAliveUnit(follow, cmd.asset); if (other != null) { follow = other; } } float tierEvt = EventStrengthFromTierHint(cmd.tier, 70f); InterestLeadKind tierLead = LeadFromTierHint(cmd.tier, InterestLeadKind.EventLed); string label = string.IsNullOrEmpty(cmd.label) ? ("Inject " + (cmd.tier ?? "scene")) : cmd.label; label = EventPresentability.EnsureSubjectLedLabel(follow, label); string keySuffix = string.IsNullOrEmpty(cmd.expect) ? label.Replace(" ", "_") : cmd.expect; ParseInterestInjectOptions( cmd.value, out bool force, out InterestLeadKind? leadOpt, out float eventStrength, out float charSig, out float maxWatch, out float ttl, out bool resumable, out int participants, out int notables); InterestLeadKind lead = leadOpt ?? tierLead; if (eventStrength < 0f) { eventStrength = tierEvt; } if (charSig < 0f) { charSig = lead == InterestLeadKind.CharacterLed ? eventStrength : 10f; } if (maxWatch < 0f) { maxWatch = 30f; } if (ttl < 0f) { ttl = 60f; } InterestCandidate c = InterestFeeds.RegisterHarness( follow, label, keySuffix, lead, force ? InterestCompletionKind.Manual : InterestCompletionKind.FixedDwell, force, eventStrength, charSig, maxWatch, ttl, related: null, resumable); if (c == null) { _cmdFail++; Emit(cmd, ok: false, detail: "inject_failed"); return; } if (!string.IsNullOrEmpty(cmd.value) && cmd.value.IndexOf("related=partner", StringComparison.OrdinalIgnoreCase) >= 0 && _happinessPartner != null && _happinessPartner.isAlive()) { c.RelatedUnit = _happinessPartner; c.RelatedId = EventFeedUtil.SafeId(_happinessPartner); InterestScoring.RecalcTotal(c); } if (participants > 0 || notables > 0) { c.ParticipantCount = participants; c.NotableParticipantCount = notables; if (participants >= 2 || c.AssetId == "live_battle") { c.Category = "Combat"; c.Verb = "fighting"; // Keep FixedDwell/Manual for harness injects - units are not actually fighting. if (!force) { c.Completion = InterestCompletionKind.FixedDwell; } } InterestScoring.RecalcTotal(c); } ParseInjectSideCounts(cmd.value, out int sideA, out int sideB); if (sideA > 0 || sideB > 0) { c.CombatSideACount = sideA; c.CombatSideBCount = sideB; c.CombatSideFrame = EnsembleFrame.SpeciesVsSpecies; if (c.ParticipantCount < sideA + sideB) { c.ParticipantCount = sideA + sideB; } c.Category = "Combat"; InterestScoring.RecalcTotal(c); } ApplyHarnessRelationshipAssetHint(c, cmd.expect); if (force) { InterestDirector.HarnessForceSession(c); } _cmdOk++; Emit( cmd, ok: true, detail: $"injected key={c.Key} score={c.TotalScore:0.#} lead={c.LeadKind} evt={c.EventStrength:0.#} char={c.CharacterSignificance:0.#} n={c.ParticipantCount}/{c.NotableParticipantCount} sides={c.CombatSideACount}/{c.CombatSideBCount} pending={InterestRegistry.PendingCount}"); } /// /// Map harness expect tokens onto relationship AssetIds used by StoryPlanner classification. /// private static void ApplyHarnessRelationshipAssetHint(InterestCandidate c, string expect) { if (c == null || string.IsNullOrEmpty(expect)) { return; } if (expect.IndexOf("just_had_child", StringComparison.OrdinalIgnoreCase) >= 0 || expect.IndexOf("parenthood", StringComparison.OrdinalIgnoreCase) >= 0) { c.HappinessEffectId = "just_had_child"; c.AssetId = "just_had_child"; c.Category = "LifeChapter"; c.Completion = InterestCompletionKind.FixedDwell; return; } if (expect.IndexOf("just_kissed", StringComparison.OrdinalIgnoreCase) >= 0 || expect.IndexOf("intimacy", StringComparison.OrdinalIgnoreCase) >= 0) { c.HappinessEffectId = "just_kissed"; c.AssetId = "just_kissed"; c.Category = "Social"; c.Completion = InterestCompletionKind.FixedDwell; return; } if (expect.IndexOf("just_found_house", StringComparison.OrdinalIgnoreCase) >= 0 || expect.IndexOf("found_house", StringComparison.OrdinalIgnoreCase) >= 0 || expect.IndexOf("finds_a_home", StringComparison.OrdinalIgnoreCase) >= 0) { c.HappinessEffectId = "just_found_house"; c.AssetId = "just_found_house"; c.Category = "LifeChapter"; c.Completion = InterestCompletionKind.FixedDwell; return; } if (expect.IndexOf("milestone_kill", StringComparison.OrdinalIgnoreCase) >= 0 || expect.IndexOf("just_killed", StringComparison.OrdinalIgnoreCase) >= 0) { c.AssetId = "milestone_kill"; c.HappinessEffectId = "just_killed"; c.Category = "Relationship"; c.Source = "chronicle"; c.Completion = InterestCompletionKind.FixedDwell; return; } if (expect.IndexOf("fallen_in_love", StringComparison.OrdinalIgnoreCase) >= 0 || expect.IndexOf("smitten", StringComparison.OrdinalIgnoreCase) >= 0) { c.HappinessEffectId = "fallen_in_love"; c.AssetId = "fallen_in_love"; c.Category = "Social"; c.Completion = InterestCompletionKind.FixedDwell; return; } if (expect.IndexOf("sexual_reproduction", StringComparison.OrdinalIgnoreCase) >= 0 || expect.IndexOf("decision_intent", StringComparison.OrdinalIgnoreCase) >= 0) { c.AssetId = "sexual_reproduction_try"; c.Source = "decision"; c.Category = "Politics"; c.Completion = InterestCompletionKind.FixedDwell; return; } if (expect.IndexOf("find_lover", StringComparison.OrdinalIgnoreCase) >= 0) { // Decision path when tagged; relationship seeking otherwise. bool asDecision = expect.IndexOf("decision", StringComparison.OrdinalIgnoreCase) >= 0; c.AssetId = "find_lover"; c.HappinessEffectId = asDecision ? "" : "find_lover"; c.Source = asDecision ? "decision" : "relationship"; c.Category = asDecision ? "Politics" : "Relationship"; c.Completion = InterestCompletionKind.FixedDwell; return; } if (expect.IndexOf("building_complete", StringComparison.OrdinalIgnoreCase) >= 0 || expect.IndexOf("tent_", StringComparison.OrdinalIgnoreCase) >= 0 || expect.IndexOf("construction", StringComparison.OrdinalIgnoreCase) >= 0) { c.AssetId = expect.IndexOf("tent_", StringComparison.OrdinalIgnoreCase) >= 0 ? "tent_human" : "building_complete"; c.Source = "building"; c.Category = "Settlement"; c.Completion = InterestCompletionKind.FixedDwell; return; } if (expect.IndexOf("taking_roots", StringComparison.OrdinalIgnoreCase) >= 0 || expect.IndexOf("soft_status", StringComparison.OrdinalIgnoreCase) >= 0) { c.StatusId = "taking_roots"; c.AssetId = "taking_roots"; c.Completion = InterestCompletionKind.StatusPhase; c.Category = "Status"; return; } if (expect.IndexOf("stunned", StringComparison.OrdinalIgnoreCase) >= 0 || expect.IndexOf("invincible", StringComparison.OrdinalIgnoreCase) >= 0) { bool inv = expect.IndexOf("invincible", StringComparison.OrdinalIgnoreCase) >= 0; c.StatusId = inv ? "invincible" : "stunned"; c.AssetId = c.StatusId; c.Source = "status"; c.Completion = InterestCompletionKind.StatusPhase; c.Category = "Status"; return; } if (expect.IndexOf("family_pack", StringComparison.OrdinalIgnoreCase) >= 0 || expect.IndexOf("pack_soft", StringComparison.OrdinalIgnoreCase) >= 0) { c.AssetId = "family_pack"; c.Source = "family"; c.Category = "Family"; c.Completion = InterestCompletionKind.FamilyPack; c.Label = string.IsNullOrEmpty(c.Label) ? "Pack - Harness (3) · gathering" : c.Label; return; } if (expect.IndexOf("just_slept", StringComparison.OrdinalIgnoreCase) >= 0 || expect.IndexOf("rest_life", StringComparison.OrdinalIgnoreCase) >= 0 || expect.IndexOf("ends a rest", StringComparison.OrdinalIgnoreCase) >= 0) { c.HappinessEffectId = "just_slept"; c.AssetId = "just_slept"; c.Source = "happiness"; c.Completion = InterestCompletionKind.FixedDwell; c.Category = "Daily"; return; } if (expect.IndexOf("sleeping", StringComparison.OrdinalIgnoreCase) >= 0 || expect.IndexOf("falls asleep", StringComparison.OrdinalIgnoreCase) >= 0) { c.StatusId = "sleeping"; c.AssetId = "sleeping"; c.Source = "status"; c.Completion = InterestCompletionKind.StatusPhase; c.Category = "Status"; return; } if (expect.IndexOf("just_got_out_of_egg", StringComparison.OrdinalIgnoreCase) >= 0 || expect.IndexOf("hatch", StringComparison.OrdinalIgnoreCase) >= 0) { c.HappinessEffectId = "just_got_out_of_egg"; c.AssetId = "just_got_out_of_egg"; c.Completion = InterestCompletionKind.FixedDwell; return; } if (expect.IndexOf("king_dead", StringComparison.OrdinalIgnoreCase) >= 0 || expect.IndexOf("worldlog_meta", StringComparison.OrdinalIgnoreCase) >= 0) { c.AssetId = "king_dead"; c.Source = "worldlog"; c.Category = "Politics"; c.Completion = InterestCompletionKind.FixedDwell; return; } if (expect.IndexOf("set_lover", StringComparison.OrdinalIgnoreCase) >= 0 || expect.IndexOf("lover", StringComparison.OrdinalIgnoreCase) >= 0) { c.AssetId = "set_lover"; c.Category = "Relationship"; } } private static Actor FindOtherAliveUnitPreferOffRoster( Actor exclude, string preferAsset, bool preferOffRoster) { Actor bestOff = null; Actor bestAny = null; if (World.world?.units == null) { return null; } foreach (Actor actor in World.world.units) { if (actor == null || !actor.isAlive() || actor == exclude) { continue; } if (!string.IsNullOrEmpty(preferAsset) && (actor.asset == null || !string.Equals(actor.asset.id, preferAsset, StringComparison.OrdinalIgnoreCase))) { continue; } long id = EventFeedUtil.SafeId(actor); if (preferOffRoster && id != 0 && !LifeSagaRoster.IsMc(id)) { bestOff = actor; break; } if (bestAny == null) { bestAny = actor; } } return bestOff ?? bestAny ?? FindOtherAliveUnit(exclude, preferAsset); } private static Actor FindOtherAliveUnit(Actor exclude, string preferAsset) { if (World.world?.units == null) { return null; } long excludeId = EventFeedUtil.SafeId(exclude); string wantAsset = (preferAsset ?? "").Trim(); if (wantAsset == "auto") { wantAsset = ""; } Actor fallback = null; try { foreach (Actor a in World.world.units) { if (a == null || !a.isAlive() || EventFeedUtil.SafeId(a) == excludeId) { continue; } string id = a.asset != null ? a.asset.id : ""; if (!string.IsNullOrEmpty(wantAsset) && string.Equals(id, wantAsset, StringComparison.OrdinalIgnoreCase)) { return a; } if (fallback == null) { fallback = a; } } } catch { // ignore } return fallback; } private static void ParseInjectSideCounts(string raw, out int sideA, out int sideB) { sideA = 0; sideB = 0; if (string.IsNullOrEmpty(raw)) { return; } string[] parts = raw.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < parts.Length; i++) { string p = parts[i].Trim(); int eq = p.IndexOf('='); if (eq <= 0) { continue; } string key = p.Substring(0, eq).Trim().ToLowerInvariant(); string val = p.Substring(eq + 1).Trim(); if (key == "sidea" || key == "a") { int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out sideA); } else if (key == "sideb" || key == "b") { int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out sideB); } } } private static void ParseInterestInjectOptions( string raw, out bool force, out InterestLeadKind? lead, out float eventStrength, out float charSig, out float maxWatch, out float ttl, out bool resumable, out int participants, out int notables) { force = false; lead = null; eventStrength = -1f; charSig = -1f; maxWatch = -1f; ttl = -1f; resumable = true; participants = 0; notables = 0; if (string.IsNullOrEmpty(raw)) { return; } string[] parts = raw.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < parts.Length; i++) { string p = parts[i].Trim(); if (p.Equals("true", StringComparison.OrdinalIgnoreCase) || p.Equals("force", StringComparison.OrdinalIgnoreCase)) { force = true; continue; } if (p.Equals("false", StringComparison.OrdinalIgnoreCase)) { force = false; continue; } int eq = p.IndexOf('='); if (eq <= 0) { if (p.Equals("event", StringComparison.OrdinalIgnoreCase) || p.Equals("eventled", StringComparison.OrdinalIgnoreCase)) { lead = InterestLeadKind.EventLed; } else if (p.Equals("character", StringComparison.OrdinalIgnoreCase) || p.Equals("char", StringComparison.OrdinalIgnoreCase) || p.Equals("characterled", StringComparison.OrdinalIgnoreCase)) { lead = InterestLeadKind.CharacterLed; } continue; } string key = p.Substring(0, eq).Trim().ToLowerInvariant(); string val = p.Substring(eq + 1).Trim(); switch (key) { case "force": force = ParseBool(val, false); break; case "lead": if (val.StartsWith("c", StringComparison.OrdinalIgnoreCase)) { lead = InterestLeadKind.CharacterLed; } else { lead = InterestLeadKind.EventLed; } break; case "evt": case "event": case "strength": eventStrength = ParseFloat(val, eventStrength); break; case "char": case "charsig": charSig = ParseFloat(val, charSig); break; case "max": case "maxwatch": maxWatch = ParseFloat(val, maxWatch); break; case "ttl": ttl = ParseFloat(val, ttl); break; case "resumable": resumable = ParseBool(val, true); break; case "n": case "participants": case "fighters": int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out participants); break; case "notables": case "notable": int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out notables); break; } } } private static void DoInterestForceSession(HarnessCommand cmd) { if (!WorldReady()) { _cmdFail++; Emit(cmd, ok: false, detail: "world_not_ready"); return; } Actor follow = ResolveUnit(string.IsNullOrEmpty(cmd.asset) || cmd.asset == "auto" ? null : cmd.asset); if (follow == null) { follow = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f); } if (follow == null) { _cmdFail++; Emit(cmd, ok: false, detail: "no unit for interest_force_session"); return; } float tierEvt = EventStrengthFromTierHint(cmd.tier, 70f); InterestLeadKind tierLead = LeadFromTierHint(cmd.tier, InterestLeadKind.EventLed); ParseInterestInjectOptions( cmd.value, out bool forceIgnored, out InterestLeadKind? leadOpt, out float eventStrength, out float charSigIgnored, out float maxWatchIgnored, out float ttlIgnored, out bool resumableIgnored, out int participantsIgnored, out int notablesIgnored); _ = forceIgnored; _ = charSigIgnored; _ = maxWatchIgnored; _ = ttlIgnored; _ = resumableIgnored; _ = participantsIgnored; _ = notablesIgnored; InterestLeadKind lead = leadOpt ?? tierLead; if (eventStrength < 0f) { eventStrength = tierEvt; } string label = string.IsNullOrEmpty(cmd.label) ? ("Force " + (cmd.tier ?? "scene")) : cmd.label; string focusName = SafeName(follow); if (!string.IsNullOrEmpty(focusName)) { label = label.Replace("{a}", focusName); } string keySuffix = string.IsNullOrEmpty(cmd.expect) ? "force_" + label.Replace(" ", "_") : cmd.expect; InterestCandidate c = InterestFeeds.RegisterHarness( follow, label, keySuffix, lead: lead, completion: InterestCompletionKind.Manual, forceActive: true, eventStrength: eventStrength > 0f ? eventStrength : 70f, maxWatch: 60f); if (c != null && lead == InterestLeadKind.CharacterLed) { c.Category = "CharacterVignette"; InterestScoring.ScoreCheap(c); } ApplyHarnessRelationshipAssetHint(c, cmd.expect); bool ok = InterestDirector.HarnessForceSession(c); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: $"key={InterestDirector.CurrentKey} score={InterestDirector.CurrentScoreLabel} active={InterestDirector.CurrentIsActive}"); } /// /// Force a CombatActive session with follow + related so handoff/tip rewrite can be asserted. /// asset = follow, value = related asset (optional), label overrides Fight prose. /// private static void DoInterestCombatSession(HarnessCommand cmd) { if (!WorldReady()) { _cmdFail++; Emit(cmd, ok: false, detail: "world_not_ready"); return; } string requestedAsset = string.IsNullOrEmpty(cmd.asset) || cmd.asset == "auto" ? null : cmd.asset; Actor focused = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; Actor follow = focused != null && focused.isAlive() && (requestedAsset == null || (focused.asset != null && focused.asset.id == requestedAsset)) ? focused : ResolveUnit(requestedAsset); if (follow == null) { follow = focused; } if (follow == null) { follow = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f); } Actor related = null; if (!string.IsNullOrEmpty(cmd.value) && cmd.value != "auto") { related = ResolveUnit(cmd.value); } if (related == null || related == follow) { foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic()) { if (actor != null && actor != follow && actor.isAlive()) { related = actor; break; } } } if (follow == null || !follow.isAlive()) { _cmdFail++; Emit(cmd, ok: false, detail: "no follow unit for interest_combat_session"); return; } string label = string.IsNullOrEmpty(cmd.label) ? EventReason.Combat(new LiveEnsemble { Kind = EnsembleKind.Combat, Scale = EnsembleScale.Pair, Frame = EnsembleFrame.NamedPair, ParticipantCount = related != null ? 2 : 1, Focus = follow, Related = related, SideA = new EnsembleSide { Display = SafeName(follow), Count = 1, Best = follow }, SideB = related != null ? new EnsembleSide { Display = SafeName(related), Count = 1, Best = related } : null }) : cmd.label.Replace("{a}", SafeName(follow)).Replace("{b}", SafeName(related)); string keySuffix = string.IsNullOrEmpty(cmd.expect) ? "combat_focus" : cmd.expect; InterestCandidate c = InterestFeeds.RegisterHarness( follow, label, keySuffix, lead: InterestLeadKind.EventLed, completion: InterestCompletionKind.CombatActive, forceActive: true, eventStrength: 120f, characterSignificance: 20f, maxWatch: 60f, ttlSeconds: 60f, related: related, resumable: true); if (c != null) { c.Category = "Combat"; c.Verb = "fighting"; c.ClearCombatSticky(); c.StampCombatPair(follow, related); if (related != null) { c.ParticipantCount = 2; // Optional: label/expect may say no_attack so ownership asserts are not race-killed. string meta = ((cmd.label ?? "") + " " + (cmd.expect ?? "")).ToLowerInvariant(); bool wireAttack = meta.IndexOf("no_attack", StringComparison.Ordinal) < 0; if (wireAttack) { TrySetAttackTarget(follow, related); TrySetAttackTarget(related, follow); } else { // Keep the pair alive through ownership asserts (fast-timing worlds are lethal). StatusGameApi.TryAddStatus(follow, "invincible", 120f); StatusGameApi.TryAddStatus(related, "invincible", 120f); } } InterestScoring.ScoreCheap(c); } bool ok = InterestDirector.HarnessForceSession(c); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"key={InterestDirector.CurrentKey} follow={SafeName(follow)} related={SafeName(related)} tip='{CameraDirector.LastWatchLabel}'"); } /// /// Force a WarFront sticky session with kingdom sides (synthetic keys when units lack civs). /// private static void DoInterestWarSession(HarnessCommand cmd) { if (!WorldReady()) { _cmdFail++; Emit(cmd, ok: false, detail: "world_not_ready"); return; } Actor follow = ResolveUnit(string.IsNullOrEmpty(cmd.asset) || cmd.asset == "auto" ? null : cmd.asset); if (follow == null) { follow = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; } if (follow == null) { follow = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f); } Actor related = null; if (!string.IsNullOrEmpty(cmd.value) && cmd.value != "auto") { related = ResolveUnit(cmd.value); } if (related == null || related == follow) { foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic()) { if (actor != null && actor != follow && actor.isAlive()) { related = actor; break; } } } if (follow == null || !follow.isAlive()) { _cmdFail++; Emit(cmd, ok: false, detail: "no follow unit for interest_war_session"); return; } string keyA = LiveEnsemble.KingdomKeyOf(follow); string keyB = related != null ? LiveEnsemble.KingdomKeyOf(related) : ""; if (string.IsNullOrEmpty(keyA)) { keyA = "Essiona"; } if (string.IsNullOrEmpty(keyB) || keyB.Equals(keyA, StringComparison.OrdinalIgnoreCase)) { keyB = "Northreach"; } var ensemble = new LiveEnsemble { Kind = EnsembleKind.WarFront, Frame = EnsembleFrame.KingdomVsKingdom, Scale = EnsembleScale.Battle, ParticipantCount = 20, Focus = follow, Related = related, SideA = new EnsembleSide { Key = keyA, Display = LiveEnsemble.KingdomDisplay(keyA), KingdomDisplay = LiveEnsemble.KingdomDisplay(keyA), Count = 12, Best = follow }, SideB = new EnsembleSide { Key = keyB, Display = LiveEnsemble.KingdomDisplay(keyB), KingdomDisplay = LiveEnsemble.KingdomDisplay(keyB), Count = 8, Best = related } }; string label = EventReason.WarFront(ensemble); string keySuffix = string.IsNullOrEmpty(cmd.expect) ? "war_front" : cmd.expect; InterestCandidate c = InterestFeeds.RegisterHarness( follow, label, keySuffix, lead: InterestLeadKind.EventLed, completion: InterestCompletionKind.WarFront, forceActive: true, eventStrength: 120f, characterSignificance: 20f, maxWatch: 60f, ttlSeconds: 60f, related: related, resumable: true); if (c != null) { c.Category = "War"; c.AssetId = "live_war"; c.ClearCombatSticky(); StickyScoreboard.TryLockFromEnsemble(c.Sticky, ensemble); c.ParticipantCount = ensemble.ParticipantCount; InterestScoring.ScoreCheap(c); } bool ok = InterestDirector.HarnessForceSession(c); if (ok && c != null) { // ForceSession clears sticky for CombatActive only; re-lock war sides. StickyScoreboard.TryLockFromEnsemble(c.Sticky, ensemble); ok = InterestDirector.HarnessApplyWarEnsemble(ensemble); } if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"key={InterestDirector.CurrentKey} follow={SafeName(follow)} tip='{CameraDirector.LastWatchLabel}' sides={keyA}/{keyB}"); } /// /// Force an EarthquakeActive disaster session so crisis chapters can gate without live quakes. /// private static void DoInterestEarthquakeSession(HarnessCommand cmd) { if (!WorldReady()) { _cmdFail++; Emit(cmd, ok: false, detail: "world_not_ready"); return; } Actor follow = ResolveUnit(string.IsNullOrEmpty(cmd.asset) || cmd.asset == "auto" ? null : cmd.asset); if (follow == null) { follow = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f); } if (follow == null || !follow.isAlive()) { _cmdFail++; Emit(cmd, ok: false, detail: "no follow unit for interest_earthquake_session"); return; } string label = EventReason.Earthquake(follow); string keySuffix = string.IsNullOrEmpty(cmd.expect) ? "earthquake" : cmd.expect; InterestCandidate c = InterestFeeds.RegisterHarness( follow, label, keySuffix, lead: InterestLeadKind.EventLed, completion: InterestCompletionKind.EarthquakeActive, forceActive: true, eventStrength: 95f, characterSignificance: 12f, maxWatch: 45f, ttlSeconds: 45f, related: null, resumable: true); if (c != null) { c.Category = "Disaster"; c.AssetId = "earthquake"; c.ClearCombatSticky(); InterestScoring.ScoreCheap(c); } bool ok = InterestDirector.HarnessForceSession(c); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"key={InterestDirector.CurrentKey} follow={SafeName(follow)} tip='{CameraDirector.LastWatchLabel}'"); } /// /// Rewrite sticky war counts on the current WarFront scene. /// value = "aCount:bCount" (default 12:8). /// private static void DoWarEnsembleApply(HarnessCommand cmd) { InterestCandidate scene = InterestDirector.CurrentCandidate; if (scene == null || scene.Completion != InterestCompletionKind.WarFront) { _cmdFail++; Emit(cmd, ok: false, detail: "no_war_scene"); return; } Actor focus = scene.FollowUnit; if (focus == null || !focus.isAlive()) { focus = scene.RelatedUnit; } if (focus == null || !focus.isAlive()) { _cmdFail++; Emit(cmd, ok: false, detail: "no_focus"); return; } int countA = 12; int countB = 8; string raw = (cmd.value ?? "").Trim(); int colon = raw.IndexOf(':'); if (colon > 0) { int.TryParse(raw.Substring(0, colon), NumberStyles.Integer, CultureInfo.InvariantCulture, out countA); int.TryParse(raw.Substring(colon + 1), NumberStyles.Integer, CultureInfo.InvariantCulture, out countB); } countA = Math.Max(0, countA); countB = Math.Max(0, countB); string keyA = !string.IsNullOrEmpty(scene.CombatSideAKey) ? scene.CombatSideAKey : "Essiona"; string keyB = !string.IsNullOrEmpty(scene.CombatSideBKey) ? scene.CombatSideBKey : "Northreach"; var ensemble = new LiveEnsemble { Kind = EnsembleKind.WarFront, Frame = EnsembleFrame.KingdomVsKingdom, Scale = LiveEnsemble.ScaleForCount(Math.Max(3, countA + countB)), ParticipantCount = countA + countB, Focus = focus, Related = scene.RelatedUnit, SideA = new EnsembleSide { Key = keyA, Display = LiveEnsemble.KingdomDisplay(keyA), KingdomDisplay = LiveEnsemble.KingdomDisplay(keyA), Count = countA, Best = focus }, SideB = new EnsembleSide { Key = keyB, Display = LiveEnsemble.KingdomDisplay(keyB), KingdomDisplay = LiveEnsemble.KingdomDisplay(keyB), Count = countB, Best = scene.RelatedUnit } }; // Keep locked keys; only rewrite counts via apply + refresh. if (!scene.HasStickyCombatSides) { StickyScoreboard.TryLockFromEnsemble(scene.Sticky, ensemble); } else { scene.Sticky.SideACount = countA; scene.Sticky.SideBCount = countB; scene.Sticky.PeakParticipants = Math.Max(scene.Sticky.PeakParticipants, countA + countB); } bool ok = InterestDirector.HarnessApplyWarEnsemble(ensemble); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"tip='{CameraDirector.LastWatchLabel}' counts={countA}:{countB} participants={scene.ParticipantCount}"); } /// /// Register a hot CombatActive Mass tip on the current WarFront kingdom pair /// (pending peer) so war_theater_hold can be gated. /// private static void DoWarInjectTheaterMass(HarnessCommand cmd) { InterestCandidate war = InterestDirector.CurrentCandidate; if (war == null || war.Completion != InterestCompletionKind.WarFront || war.Sticky == null || !war.Sticky.HasOpposingSides) { _cmdFail++; Emit(cmd, ok: false, detail: "no_war_theater"); return; } // Copy the war sticky keys verbatim so SharesKingdomTheater cannot miss on aliases. string keyA = war.CombatSideAKey; string keyB = war.CombatSideBKey; string dispA = !string.IsNullOrEmpty(war.CombatSideADisplay) ? war.CombatSideADisplay : LiveEnsemble.KingdomDisplay(keyA); string dispB = !string.IsNullOrEmpty(war.CombatSideBDisplay) ? war.CombatSideBDisplay : LiveEnsemble.KingdomDisplay(keyB); if (string.IsNullOrEmpty(keyA) || string.IsNullOrEmpty(keyB)) { _cmdFail++; Emit(cmd, ok: false, detail: "empty_war_keys"); return; } Actor focus = war.FollowUnit; Actor related = war.RelatedUnit; if (focus == null || !focus.isAlive()) { _cmdFail++; Emit(cmd, ok: false, detail: "no_war_follow"); return; } int countA = 8; int countB = 6; string raw = (cmd.value ?? "").Trim(); int colon = raw.IndexOf(':'); if (colon > 0) { int.TryParse(raw.Substring(0, colon), NumberStyles.Integer, CultureInfo.InvariantCulture, out countA); int.TryParse(raw.Substring(colon + 1), NumberStyles.Integer, CultureInfo.InvariantCulture, out countB); } countA = Math.Max(1, countA); countB = Math.Max(1, countB); var ensemble = new LiveEnsemble { Kind = EnsembleKind.Combat, Frame = EnsembleFrame.KingdomVsKingdom, Scale = EnsembleScale.Mass, ParticipantCount = countA + countB, Focus = focus, Related = related, SideA = new EnsembleSide { Key = keyA, Display = dispA, KingdomDisplay = dispA, Count = countA, Best = focus }, SideB = new EnsembleSide { Key = keyB, Display = dispB, KingdomDisplay = dispB, Count = countB, Best = related } }; string label = EventReason.Combat(ensemble); InterestCandidate mass = InterestFeeds.RegisterHarness( focus, label, "war_theater_mass", lead: InterestLeadKind.EventLed, completion: InterestCompletionKind.CombatActive, forceActive: false, eventStrength: 140f, characterSignificance: 25f, maxWatch: 40f, ttlSeconds: 40f, related: related, resumable: true); if (mass == null) { _cmdFail++; Emit(cmd, ok: false, detail: "register_failed"); return; } mass.Category = "Combat"; mass.AssetId = "live_battle"; mass.ClearCombatSticky(); StickyScoreboard.TryLockFromEnsemble(mass.Sticky, ensemble); mass.ParticipantCount = ensemble.ParticipantCount; mass.Label = label; InterestScoring.ScoreCheap(mass); _cmdOk++; Emit( cmd, ok: true, detail: $"pending mass tip='{label}' theater={keyA}/{keyB} evt={mass.EventStrength:0.#} warTip='{CameraDirector.LastWatchLabel}'"); } /// /// Force a sticky StatusOutbreak session: Outbreak - Status. /// value = status id (default cursed). Seeds live cursed carriers so maintain recounts stick. /// private static void DoInterestOutbreakSession(HarnessCommand cmd) { Actor follow = ResolveUnit(cmd.asset); if (follow == null || !follow.isAlive()) { follow = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f); } if (follow == null || !follow.isAlive()) { _cmdFail++; Emit(cmd, ok: false, detail: "no follow unit for interest_outbreak_session"); return; } string statusId = !string.IsNullOrEmpty(cmd.value) ? cmd.value.Trim() : "cursed"; if (!StatusOutbreakFeed.IsOutbreakWorthy(statusId)) { _cmdFail++; Emit(cmd, ok: false, detail: "not_outbreak_worthy status=" + statusId); return; } var carriers = EnsureStatusCarriers(follow, statusId, want: 4); if (carriers.Count < 2) { _cmdFail++; Emit(cmd, ok: false, detail: "could not seed outbreak carriers status=" + statusId); return; } Actor related = carriers.Count > 1 ? carriers[1] : null; string keyA = "status:" + statusId; int count = carriers.Count; var ensemble = new LiveEnsemble { Kind = EnsembleKind.StatusOutbreak, Frame = EnsembleFrame.SpeciesVsSpecies, Scale = LiveEnsemble.ScaleForCount(Math.Max(2, count)), ParticipantCount = count, Focus = follow, Related = related, SideA = new EnsembleSide { Key = keyA, Display = LiveEnsemble.StatusDisplayName(statusId), Count = count, Best = follow } }; for (int i = 0; i < carriers.Count; i++) { long id = EventFeedUtil.SafeId(carriers[i]); if (id != 0) { ensemble.ParticipantIds.Add(id); } } string label = EventReason.StatusOutbreak(ensemble); string keySuffix = string.IsNullOrEmpty(cmd.expect) ? "status_outbreak" : cmd.expect; InterestCandidate c = InterestFeeds.RegisterHarness( follow, label, keySuffix, lead: InterestLeadKind.EventLed, completion: InterestCompletionKind.StatusOutbreak, forceActive: true, eventStrength: 108f, characterSignificance: 18f, maxWatch: 45f, ttlSeconds: 45f, related: related, resumable: true); if (c != null) { c.Category = "StatusTransformation"; c.AssetId = "live_outbreak"; c.StatusId = statusId; c.ClearCombatSticky(); StickyScoreboard.TryLockFromEnsemble(c.Sticky, ensemble); c.ParticipantCount = ensemble.ParticipantCount; InterestScoring.ScoreCheap(c); } bool ok = InterestDirector.HarnessForceSession(c); if (ok && c != null) { StickyScoreboard.TryLockFromEnsemble(c.Sticky, ensemble); ok = InterestDirector.HarnessApplyStatusOutbreakEnsemble(ensemble); } if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"key={InterestDirector.CurrentKey} follow={SafeName(follow)} tip='{CameraDirector.LastWatchLabel}' status={statusId} carriers={count}"); } /// /// Rewrite sticky outbreak carrier count from live cursed units. value = count (default 4). /// private static void DoOutbreakEnsembleApply(HarnessCommand cmd) { InterestCandidate scene = InterestDirector.CurrentCandidate; if (scene == null || scene.Completion != InterestCompletionKind.StatusOutbreak) { _cmdFail++; Emit(cmd, ok: false, detail: "no_outbreak_scene"); return; } Actor focus = scene.FollowUnit; if (focus == null || !focus.isAlive()) { focus = scene.RelatedUnit; } if (focus == null || !focus.isAlive()) { _cmdFail++; Emit(cmd, ok: false, detail: "no_focus"); return; } int want = 4; string raw = (cmd.value ?? "").Trim(); if (!string.IsNullOrEmpty(raw)) { int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out want); } want = Math.Max(0, want); string statusId = !string.IsNullOrEmpty(scene.StatusId) ? scene.StatusId : "cursed"; var carriers = EnsureStatusCarriers(focus, statusId, want); int count = carriers.Count; Actor related = null; for (int i = 0; i < carriers.Count; i++) { if (carriers[i] != null && carriers[i] != focus && carriers[i].isAlive()) { related = carriers[i]; break; } } string keyA = !string.IsNullOrEmpty(scene.CombatSideAKey) ? scene.CombatSideAKey : "status:" + statusId; string display = !string.IsNullOrEmpty(scene.Sticky?.SideADisplay) ? scene.Sticky.SideADisplay : LiveEnsemble.StatusDisplayName(statusId); var ensemble = new LiveEnsemble { Kind = EnsembleKind.StatusOutbreak, Frame = EnsembleFrame.SpeciesVsSpecies, Scale = LiveEnsemble.ScaleForCount(Math.Max(2, count)), ParticipantCount = count, Focus = focus, Related = related ?? scene.RelatedUnit, SideA = new EnsembleSide { Key = keyA, Display = display, Count = count, Best = focus } }; for (int i = 0; i < carriers.Count; i++) { long id = EventFeedUtil.SafeId(carriers[i]); if (id != 0) { ensemble.ParticipantIds.Add(id); } } if (scene.Sticky != null) { scene.Sticky.SideAIds.Clear(); for (int i = 0; i < carriers.Count; i++) { long id = EventFeedUtil.SafeId(carriers[i]); if (id != 0) { scene.Sticky.SideAIds.Add(id); } } scene.Sticky.SideACount = count; scene.Sticky.SideADisplay = display; scene.Sticky.PeakParticipants = Math.Max(scene.Sticky.PeakParticipants, count); } if (!scene.HasStickyCombatSides) { StickyScoreboard.TryLockFromEnsemble(scene.Sticky, ensemble); } bool ok = InterestDirector.HarnessApplyStatusOutbreakEnsemble(ensemble); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"tip='{CameraDirector.LastWatchLabel}' count={count} want={want} participants={scene.ParticipantCount}"); } /// /// Ensure exactly living units near carry /// (clears extras so maintain/CollectStatusCarriers cannot inflate). /// Spawns same-species units when the map is short. /// private static List EnsureStatusCarriers(Actor focus, string statusId, int want) { var keep = new List(); if (focus == null || !focus.isAlive() || string.IsNullOrEmpty(statusId) || want <= 0) { return keep; } StatusGameApi.TryAddStatus(focus, statusId, overrideTimer: 120f); if (StatusGameApi.HasStatus(focus, statusId)) { keep.Add(focus); } string assetId = focus.asset != null ? focus.asset.id : "human"; WorldTile tile = null; try { tile = World.world.GetTile( Mathf.RoundToInt(focus.current_position.x), Mathf.RoundToInt(focus.current_position.y)); } catch { tile = TileNearCamera(); } var nearby = new List(); foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic()) { if (actor == null || actor == focus || !actor.isAlive()) { continue; } nearby.Add(actor); } for (int i = 0; i < nearby.Count && keep.Count < want; i++) { Actor actor = nearby[i]; StatusGameApi.TryAddStatus(actor, statusId, overrideTimer: 120f); if (StatusGameApi.HasStatus(actor, statusId) && !keep.Contains(actor)) { keep.Add(actor); } } int guard = 0; while (keep.Count < want && tile != null && guard++ < want + 8) { Actor spawned = World.world.units.spawnNewUnit( assetId, tile, pSpawnSound: false, pMiracleSpawn: true); if (spawned == null || !spawned.isAlive()) { continue; } StatusGameApi.TryAddStatus(spawned, statusId, overrideTimer: 120f); if (StatusGameApi.HasStatus(spawned, statusId) && !keep.Contains(spawned)) { keep.Add(spawned); } } // Clear the status from everyone else so Refresh cannot re-inflate the tip. foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic()) { if (actor == null || !actor.isAlive() || keep.Contains(actor)) { continue; } if (StatusGameApi.HasStatus(actor, statusId)) { StatusGameApi.TryFinishStatus(actor, statusId); } } return keep; } /// /// Ensure living same-species units are enrolled as a pack roster. /// Spawns when needed so maintain recounts keep the tip count. /// private static List EnsurePackMembers(Actor focus, int want) { var members = new List(); if (focus == null || !focus.isAlive() || want <= 0) { return members; } members.Add(focus); string species = LiveEnsemble.SpeciesKeyOf(focus); string assetId = focus.asset != null ? focus.asset.id : "human"; WorldTile tile = null; try { tile = World.world.GetTile( Mathf.RoundToInt(focus.current_position.x), Mathf.RoundToInt(focus.current_position.y)); } catch { tile = TileNearCamera(); } foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic()) { if (members.Count >= want) { break; } if (actor == null || actor == focus || !actor.isAlive()) { continue; } if (!string.IsNullOrEmpty(species) && !string.Equals(LiveEnsemble.SpeciesKeyOf(actor), species, StringComparison.OrdinalIgnoreCase)) { continue; } members.Add(actor); } int guard = 0; while (members.Count < want && tile != null && guard++ < want + 8) { Actor spawned = World.world.units.spawnNewUnit( assetId, tile, pSpawnSound: false, pMiracleSpawn: true); if (spawned != null && spawned.isAlive() && !members.Contains(spawned)) { members.Add(spawned); } } if (members.Count > want) { members.RemoveRange(want, members.Count - want); } return members; } /// /// Force a sticky FamilyPack session: Pack - Species (n) · gathering. /// Seeds a live same-species roster so maintain recounts keep the tip count. /// private static void DoInterestFamilySession(HarnessCommand cmd) { Actor follow = ResolveUnit(cmd.asset); if (follow == null || !follow.isAlive()) { follow = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f); } if (follow == null || !follow.isAlive()) { _cmdFail++; Emit(cmd, ok: false, detail: "no follow unit for interest_family_session"); return; } var members = EnsurePackMembers(follow, want: 5); if (members.Count < 2) { _cmdFail++; Emit(cmd, ok: false, detail: "could not seed pack members"); return; } Actor related = members.Count > 1 ? members[1] : null; if (!string.IsNullOrEmpty(cmd.value) && cmd.value != "auto") { Actor named = ResolveUnit(cmd.value); if (named != null && named.isAlive() && named != follow) { related = named; if (!members.Contains(named)) { members.Add(named); } } } string speciesKey = LiveEnsemble.SpeciesKeyOf(follow); if (string.IsNullOrEmpty(speciesKey)) { speciesKey = "human"; } string packKey = "family:harness"; int count = members.Count; var ensemble = new LiveEnsemble { Kind = EnsembleKind.FamilyPack, Frame = EnsembleFrame.SpeciesVsSpecies, Scale = LiveEnsemble.ScaleForCount(Math.Max(2, count)), ParticipantCount = count, Focus = follow, Related = related, SideA = new EnsembleSide { Key = packKey, Display = LiveEnsemble.SpeciesDisplay(speciesKey), Count = count, Best = follow } }; for (int i = 0; i < members.Count; i++) { long id = EventFeedUtil.SafeId(members[i]); if (id != 0) { ensemble.ParticipantIds.Add(id); } } string label = EventReason.FamilyPack(ensemble); string keySuffix = string.IsNullOrEmpty(cmd.expect) ? "family_pack" : cmd.expect; InterestCandidate c = InterestFeeds.RegisterHarness( follow, label, keySuffix, lead: InterestLeadKind.EventLed, completion: InterestCompletionKind.FamilyPack, forceActive: true, // Match live soft pack band so unit events can still cut in during playtests. eventStrength: 48f, characterSignificance: 12f, maxWatch: 12f, ttlSeconds: 20f, related: related, resumable: true); if (c != null) { c.Category = "Relationship"; c.AssetId = "live_family"; c.ClearCombatSticky(); StickyScoreboard.TryLockFromEnsemble(c.Sticky, ensemble); c.ParticipantCount = ensemble.ParticipantCount; InterestScoring.ScoreCheap(c); } bool ok = InterestDirector.HarnessForceSession(c); if (ok && c != null) { StickyScoreboard.TryLockFromEnsemble(c.Sticky, ensemble); ok = InterestDirector.HarnessApplyFamilyEnsemble(ensemble); } if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"key={InterestDirector.CurrentKey} follow={SafeName(follow)} tip='{CameraDirector.LastWatchLabel}' pack={packKey} members={count}"); } /// /// Rewrite sticky family pack count from a live same-species roster. /// value = member count (default 5). /// private static void DoFamilyEnsembleApply(HarnessCommand cmd) { InterestCandidate scene = InterestDirector.CurrentCandidate; if (scene == null || scene.Completion != InterestCompletionKind.FamilyPack) { _cmdFail++; Emit(cmd, ok: false, detail: "no_family_scene"); return; } Actor focus = scene.FollowUnit; if (focus == null || !focus.isAlive()) { focus = scene.RelatedUnit; } if (focus == null || !focus.isAlive()) { _cmdFail++; Emit(cmd, ok: false, detail: "no_focus"); return; } int want = 5; string raw = (cmd.value ?? "").Trim(); if (!string.IsNullOrEmpty(raw)) { int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out want); } want = Math.Max(0, want); var members = EnsurePackMembers(focus, want); int count = members.Count; Actor related = null; for (int i = 0; i < members.Count; i++) { if (members[i] != null && members[i] != focus && members[i].isAlive()) { related = members[i]; break; } } // Solo dip: drop related so Refresh/SeedFromEnsemble cannot re-inflate to 2. if (count <= 1) { related = null; scene.RelatedUnit = null; scene.RelatedId = 0; } string keyA = !string.IsNullOrEmpty(scene.CombatSideAKey) ? scene.CombatSideAKey : "family:harness"; string species = LiveEnsemble.SpeciesKeyOf(focus); string display = !string.IsNullOrEmpty(scene.Sticky?.SideADisplay) ? scene.Sticky.SideADisplay : LiveEnsemble.SpeciesDisplay(species); var ensemble = new LiveEnsemble { Kind = EnsembleKind.FamilyPack, Frame = EnsembleFrame.SpeciesVsSpecies, Scale = LiveEnsemble.ScaleForCount(Math.Max(2, count)), ParticipantCount = count, Focus = focus, Related = related, SideA = new EnsembleSide { Key = keyA, Display = display, Count = count, Best = focus } }; for (int i = 0; i < members.Count; i++) { long id = EventFeedUtil.SafeId(members[i]); if (id != 0) { ensemble.ParticipantIds.Add(id); } } if (scene.Sticky != null) { scene.Sticky.SideAIds.Clear(); for (int i = 0; i < members.Count; i++) { long id = EventFeedUtil.SafeId(members[i]); if (id != 0) { scene.Sticky.SideAIds.Add(id); } } scene.Sticky.SideACount = count; scene.Sticky.SideADisplay = display; scene.Sticky.PeakParticipants = Math.Max(scene.Sticky.PeakParticipants, count); } if (!scene.HasStickyCombatSides) { StickyScoreboard.TryLockFromEnsemble(scene.Sticky, ensemble); } bool ok = InterestDirector.HarnessApplyFamilyEnsemble(ensemble); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"tip='{CameraDirector.LastWatchLabel}' count={count} want={want} participants={scene.ParticipantCount}"); } /// /// Force a sticky PlotActive session: Plotters vs a target kingdom key. /// private static void DoInterestPlotSession(HarnessCommand cmd) { Actor follow = ResolveUnit(cmd.asset); if (follow == null || !follow.isAlive()) { follow = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f); } Actor related = null; if (!string.IsNullOrEmpty(cmd.value) && cmd.value != "auto") { related = ResolveUnit(cmd.value); } if (related == null || related == follow) { foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic()) { if (actor != null && actor != follow && actor.isAlive()) { related = actor; break; } } } if (follow == null || !follow.isAlive()) { _cmdFail++; Emit(cmd, ok: false, detail: "no follow unit for interest_plot_session"); return; } // Seed a real plotter roster (2+) so PlotCell sticky / completion stay group-shaped. var plotters = EnsurePackMembers(follow, want: 3); if (plotters.Count < 2) { _cmdFail++; Emit(cmd, ok: false, detail: "could not seed plotters"); return; } string keyA = "plot:rebellion"; string keyB = related != null ? LiveEnsemble.KingdomKeyOf(related) : ""; if (string.IsNullOrEmpty(keyB) || LiveEnsemble.IsPlotterSideKey(keyB)) { keyB = "Essiona"; } int countA = plotters.Count; var ensemble = new LiveEnsemble { Kind = EnsembleKind.PlotCell, Frame = EnsembleFrame.KingdomVsKingdom, Scale = EnsembleScale.Skirmish, ParticipantCount = countA + 12, Focus = follow, Related = related, SideA = new EnsembleSide { Key = keyA, Display = "Plotters", Count = countA, Best = follow }, SideB = new EnsembleSide { Key = keyB, Display = LiveEnsemble.KingdomDisplay(keyB), KingdomDisplay = LiveEnsemble.KingdomDisplay(keyB), Count = 12, Best = related } }; for (int i = 0; i < plotters.Count; i++) { long id = EventFeedUtil.SafeId(plotters[i]); if (id != 0) { ensemble.ParticipantIds.Add(id); } } string label = EventReason.PlotCell(ensemble); string keySuffix = string.IsNullOrEmpty(cmd.expect) ? "plot_cell" : cmd.expect; InterestCandidate c = InterestFeeds.RegisterHarness( follow, label, keySuffix, lead: InterestLeadKind.EventLed, completion: InterestCompletionKind.PlotActive, forceActive: true, eventStrength: 115f, characterSignificance: 20f, maxWatch: 50f, ttlSeconds: 50f, related: related, resumable: true); if (c != null) { c.Category = "Politics"; c.AssetId = "live_plot"; c.ClearCombatSticky(); StickyScoreboard.TryLockFromEnsemble(c.Sticky, ensemble); c.ParticipantCount = ensemble.ParticipantCount; InterestScoring.ScoreCheap(c); } bool ok = InterestDirector.HarnessForceSession(c); if (ok && c != null) { StickyScoreboard.TryLockFromEnsemble(c.Sticky, ensemble); ok = InterestDirector.HarnessApplyPlotEnsemble(ensemble); } if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"key={InterestDirector.CurrentKey} follow={SafeName(follow)} tip='{CameraDirector.LastWatchLabel}' sides={keyA}/{keyB}"); } /// /// Rewrite sticky plot counts on the current PlotActive scene. /// value = "aCount:bCount" (default 3:12). /// private static void DoPlotEnsembleApply(HarnessCommand cmd) { InterestCandidate scene = InterestDirector.CurrentCandidate; if (scene == null || scene.Completion != InterestCompletionKind.PlotActive) { _cmdFail++; Emit(cmd, ok: false, detail: "no_plot_scene"); return; } Actor focus = scene.FollowUnit; if (focus == null || !focus.isAlive()) { focus = scene.RelatedUnit; } if (focus == null || !focus.isAlive()) { _cmdFail++; Emit(cmd, ok: false, detail: "no_focus"); return; } int countA = 3; int countB = 12; string raw = (cmd.value ?? "").Trim(); int colon = raw.IndexOf(':'); if (colon > 0) { int.TryParse(raw.Substring(0, colon), NumberStyles.Integer, CultureInfo.InvariantCulture, out countA); int.TryParse(raw.Substring(colon + 1), NumberStyles.Integer, CultureInfo.InvariantCulture, out countB); } countA = Math.Max(0, countA); countB = Math.Max(0, countB); // Resize the living plotter roster so maintain recount matches the requested count. var plotters = EnsurePackMembers(focus, want: Math.Max(1, countA)); if (countA >= 2 && plotters.Count < 2) { _cmdFail++; Emit(cmd, ok: false, detail: "could not seed plotters for count=" + countA); return; } if (plotters.Count > countA) { plotters.RemoveRange(countA, plotters.Count - countA); } countA = plotters.Count; string keyA = !string.IsNullOrEmpty(scene.CombatSideAKey) ? scene.CombatSideAKey : "plot:rebellion"; string keyB = !string.IsNullOrEmpty(scene.CombatSideBKey) ? scene.CombatSideBKey : "Essiona"; var ensemble = new LiveEnsemble { Kind = EnsembleKind.PlotCell, Frame = EnsembleFrame.KingdomVsKingdom, Scale = LiveEnsemble.ScaleForCount(Math.Max(3, countA + countB)), ParticipantCount = countA + countB, Focus = focus, Related = scene.RelatedUnit, SideA = new EnsembleSide { Key = keyA, Display = "Plotters", Count = countA, Best = focus }, SideB = new EnsembleSide { Key = keyB, Display = LiveEnsemble.KingdomDisplay(keyB), KingdomDisplay = LiveEnsemble.KingdomDisplay(keyB), Count = countB, Best = scene.RelatedUnit } }; for (int i = 0; i < plotters.Count; i++) { long id = EventFeedUtil.SafeId(plotters[i]); if (id != 0) { ensemble.ParticipantIds.Add(id); } } if (scene.Sticky != null) { scene.Sticky.SideAIds.Clear(); for (int i = 0; i < plotters.Count; i++) { long id = EventFeedUtil.SafeId(plotters[i]); if (id != 0) { scene.Sticky.SideAIds.Add(id); } } scene.Sticky.SideACount = countA; scene.Sticky.SideBCount = countB; scene.Sticky.SideADisplay = "Plotters"; scene.Sticky.PeakParticipants = Math.Max(scene.Sticky.PeakParticipants, countA + countB); } if (!scene.HasStickyCombatSides) { StickyScoreboard.TryLockFromEnsemble(scene.Sticky, ensemble); } bool ok = InterestDirector.HarnessApplyPlotEnsemble(ensemble); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"tip='{CameraDirector.LastWatchLabel}' counts={countA}:{countB} participants={scene.ParticipantCount}"); } /// /// Grow the current CombatActive scene into a sided multi-fighter ensemble reason. /// value = participant count (default 6). Spawns extra follow/related species near the focus. /// private static void DoCombatEnsembleEscalate(HarnessCommand cmd) { InterestCandidate scene = InterestDirector.CurrentCandidate; if (scene == null || scene.Completion != InterestCompletionKind.CombatActive) { _cmdFail++; Emit(cmd, ok: false, detail: "no_combat_scene"); return; } Actor focus = scene.FollowUnit; if (focus == null || !focus.isAlive()) { focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; } Actor related = scene.RelatedUnit; if ((focus == null || !focus.isAlive()) && related != null && related.isAlive()) { focus = related; related = null; } if (focus == null || !focus.isAlive()) { // Last resort: nearest living unit so escalate can still seed a pack. focus = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f); } if (focus == null || !focus.isAlive()) { _cmdFail++; Emit(cmd, ok: false, detail: "no_focus"); return; } // spawn_only: seed camps near the Duel without rewriting tip/sticky (natural escalate gate). bool spawnOnly = (cmd.expect ?? "").IndexOf("spawn_only", StringComparison.OrdinalIgnoreCase) >= 0 || string.Equals(cmd.label, "spawn_only", StringComparison.OrdinalIgnoreCase); if (!spawnOnly) { scene.ClearCombatSticky(); } int want = 6; if (!string.IsNullOrEmpty(cmd.value) && !int.TryParse(cmd.value, out want)) { want = 6; } want = Mathf.Clamp(want, 3, 24); string focusSpecies = focus.asset != null ? focus.asset.id : "human"; string relatedSpecies = related?.asset != null ? related.asset.id : "wolf"; if (string.Equals(relatedSpecies, focusSpecies, StringComparison.OrdinalIgnoreCase)) { relatedSpecies = focusSpecies == "human" ? "wolf" : "human"; } int sideA = want / 2; int sideB = want - sideA; // Already have focus (+ optional related); spawn the rest near the fight. for (int i = 1; i < sideA; i++) { SpawnAssetNear(focusSpecies, focus.current_position, 2f + i * 0.4f); } if (related == null || !related.isAlive()) { related = SpawnAssetNear(relatedSpecies, focus.current_position, 2.5f); } for (int i = 1; i < sideB; i++) { SpawnAssetNear(relatedSpecies, focus.current_position, 3f + i * 0.4f); } if (spawnOnly) { _cmdOk++; Emit( cmd, ok: true, detail: $"spawn_only n={want} tip='{CameraDirector.LastWatchLabel}' focus={SafeName(focus)}"); return; } var ensemble = new LiveEnsemble { Kind = EnsembleKind.Combat, Scale = LiveEnsemble.ScaleForCount(want), Frame = EnsembleFrame.SpeciesVsSpecies, ParticipantCount = want, NotableParticipantCount = Mathf.Max(scene.NotableParticipantCount, 0), Focus = focus, Related = related, Anchor = focus.current_position, SideA = new EnsembleSide { Key = focusSpecies, Display = LiveEnsemble.SpeciesDisplay(focusSpecies), Count = sideA, Best = focus }, SideB = new EnsembleSide { Key = relatedSpecies, Display = LiveEnsemble.SpeciesDisplay(relatedSpecies), Count = sideB, Best = related } }; ensemble.ParticipantIds.Add(EventFeedUtil.SafeId(focus)); if (related != null) { ensemble.ParticipantIds.Add(EventFeedUtil.SafeId(related)); } bool ok = InterestDirector.HarnessApplyCombatEnsemble(ensemble); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"n={want} scale={ensemble.Scale} frame={ensemble.Frame} tip='{CameraDirector.LastWatchLabel}' focus={SafeName(focus)} participants={InterestDirector.CurrentCandidate?.ParticipantCount}"); } /// /// Point camera Follow at another living unit without rewriting SubjectId / Label. /// Proves life-tip reason principals reject bystander dossiers under a named tip. /// asset = optional species for the bystander (default human). /// private static void DoInterestHijackFollow(HarnessCommand cmd) { InterestCandidate scene = InterestDirector.CurrentCandidate; if (scene == null) { _cmdFail++; Emit(cmd, ok: false, detail: "no_scene"); return; } long subjectId = scene.SubjectId != 0 ? scene.SubjectId : EventFeedUtil.SafeId(scene.FollowUnit); Actor bystander = null; string wantAsset = string.IsNullOrEmpty(cmd.asset) ? "human" : cmd.asset.Trim(); foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic()) { if (actor == null || !actor.isAlive()) { continue; } long id = EventFeedUtil.SafeId(actor); if (id == 0 || id == subjectId || id == scene.RelatedId) { continue; } string species = actor.asset != null ? actor.asset.id : ""; if (!string.IsNullOrEmpty(wantAsset) && !species.Equals(wantAsset, StringComparison.OrdinalIgnoreCase)) { if (bystander == null) { bystander = actor; } continue; } bystander = actor; break; } if (bystander == null) { bystander = SpawnAssetNear( wantAsset, scene.FollowUnit != null ? scene.FollowUnit.current_position : CameraPos(), 8f); } if (bystander == null || !bystander.isAlive()) { _cmdFail++; Emit(cmd, ok: false, detail: "no_bystander"); return; } // Keep SubjectId / Label on the original tip owner; only Follow + camera move. if (scene.SubjectId == 0) { scene.SubjectId = subjectId; } scene.FollowUnit = bystander; try { scene.Position = bystander.current_position; } catch { // keep } CameraDirector.FocusUnit(bystander); _cmdOk++; Emit( cmd, ok: true, detail: $"hijackFollow={SafeName(bystander)} subjectId={scene.SubjectId} relatedId={scene.RelatedId} tip='{scene.Label}'"); } /// /// Spawn a non-fighting same-side unit, park combat Follow on them (rostered bystander), /// while peers keep fighting. Maintain must retarget off the chore unit. /// private static void DoCombatParkBystanderFollow(HarnessCommand cmd) { InterestCandidate scene = InterestDirector.CurrentCandidate; if (scene == null || scene.Completion != InterestCompletionKind.CombatActive) { _cmdFail++; Emit(cmd, ok: false, detail: "no_combat_scene"); return; } Actor anchor = scene.FollowUnit; if (anchor == null || !anchor.isAlive()) { anchor = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; } if (anchor == null || !anchor.isAlive()) { _cmdFail++; Emit(cmd, ok: false, detail: "no_anchor"); return; } string sideA = string.IsNullOrEmpty(cmd.asset) ? "human" : cmd.asset.Trim(); // Spawn a bit off the scrap so wolves do not instantly re-acquire the bystander. Actor bystander = SpawnAssetNear(sideA, anchor.current_position, 6f); if (bystander == null || !bystander.isAlive()) { _cmdFail++; Emit(cmd, ok: false, detail: "spawn_failed"); return; } StatusGameApi.TryAddStatus(bystander, "invincible", 12f); StatusGameApi.TryAddStatus(bystander, "sleeping", 8f); ClearAttackTarget(bystander); TryClearCombatTask(bystander); ClearAttackTarget(bystander); long id = EventFeedUtil.SafeId(bystander); if (id != 0 && scene.Sticky != null) { if (!scene.Sticky.SideAIds.Contains(id)) { scene.Sticky.SideAIds.Add(id); } scene.Sticky.SideACount = Math.Max(scene.Sticky.SideACount, scene.Sticky.SideAIds.Count); } scene.FollowUnit = bystander; scene.SubjectId = id; try { scene.Position = bystander.current_position; } catch { // keep } CameraDirector.FocusUnit(bystander); bool fighting = LiveEnsemble.IsCombatParticipant(bystander); _cmdOk++; Emit( cmd, ok: true, detail: $"parked={SafeName(bystander)} fighting={fighting} tip='{CameraDirector.LastWatchLabel}'"); } private static Actor FindNearestAliveAsset( string assetId, bool preferUnfocused = false, Actor otherThan = null) { if (string.IsNullOrEmpty(assetId)) { return null; } try { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; Vector3 origin = CameraPos(); Actor best = null; float bestDist = float.MaxValue; foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic()) { if (actor == null || actor == otherThan || actor.asset == null) { continue; } try { if (!actor.isAlive()) { continue; } if (!string.Equals(actor.asset.id, assetId, StringComparison.OrdinalIgnoreCase)) { continue; } if (preferUnfocused && focus != null && actor == focus) { continue; } Vector3 p = actor.current_position; float dx = p.x - origin.x; float dy = p.y - origin.y; float d2 = dx * dx + dy * dy; if (d2 < bestDist) { bestDist = d2; best = actor; } } catch { // skip unstable actor } } return best; } catch { return null; } } private static void ClearAttackTarget(Actor unit) { if (unit == null) { return; } try { var mi = typeof(Actor).GetMethod( "setAttackTarget", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic); if (mi != null) { mi.Invoke(unit, new object[] { null }); return; } var prop = typeof(Actor).GetProperty( "attack_target", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic); prop?.SetValue(unit, null, null); } catch { // best-effort } } private static void TryClearCombatTask(Actor unit) { if (unit == null) { return; } try { if (!unit.hasTask() || unit.ai?.task == null || !unit.ai.task.in_combat) { return; } unit.setTask((string)null); } catch { try { unit.setTask(""); } catch { // best-effort } } } /// /// Kill living units near the combat pair so ambient packs cannot escalate a 1v1 Duel tip. /// value = radius (default 18). Keeps Follow + Related (+ optional PairOwner/Partner ids). /// private static void DoCombatIsolatePair(HarnessCommand cmd) { InterestCandidate scene = InterestDirector.CurrentCandidate; Actor keepA = scene?.FollowUnit; Actor keepB = scene?.RelatedUnit; if (keepA == null || !keepA.isAlive()) { keepA = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; } if ((keepB == null || !keepB.isAlive()) && scene != null) { if (scene.PairPartnerId != 0) { keepB = LiveEnsemble.FindTrackedActor(scene.PairPartnerId); } if ((keepB == null || !keepB.isAlive()) && scene.PairOwnerId != 0) { Actor owner = LiveEnsemble.FindTrackedActor(scene.PairOwnerId); if (owner != null && owner != keepA && owner.isAlive()) { keepB = owner; } } } if (keepA == null || !keepA.isAlive()) { _cmdFail++; Emit(cmd, ok: false, detail: "no_pair"); return; } float radius = 18f; if (!string.IsNullOrEmpty(cmd.value) && float.TryParse(cmd.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsed) && parsed > 1f) { radius = parsed; } float r2 = radius * radius; Vector2 origin = keepA.current_position; int culled = 0; var doomed = new List(32); foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic()) { if (actor == null || !actor.isAlive() || actor == keepA || actor == keepB) { continue; } try { Vector2 d = actor.current_position - origin; if (d.sqrMagnitude > r2) { continue; } } catch { continue; } doomed.Add(actor); } for (int i = 0; i < doomed.Count; i++) { try { doomed[i].die(true, AttackType.Divine); culled++; } catch { // keep going } } if (scene != null) { scene.ClearCombatSticky(); scene.ParticipantCount = keepB != null && keepB.isAlive() ? 2 : 1; scene.CombatPeakParticipants = scene.ParticipantCount; } _cmdOk++; Emit( cmd, ok: true, detail: $"culled={culled} keepA={SafeName(keepA)} keepB={SafeName(keepB)} r={radius:0.#} tip='{CameraDirector.LastWatchLabel}'"); } /// /// Best-effort: set attack_target across two species camps near the combat focus /// so sees a real multi-fighter cluster. /// asset = side A species (default human), value = side B species (default wolf). /// private static void DoCombatWireAttackSides(HarnessCommand cmd) { InterestCandidate scene = InterestDirector.CurrentCandidate; Actor anchor = scene?.FollowUnit; if (anchor == null || !anchor.isAlive()) { anchor = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; } if (anchor == null || !anchor.isAlive()) { _cmdFail++; Emit(cmd, ok: false, detail: "no_anchor"); return; } string sideA = string.IsNullOrEmpty(cmd.asset) ? "human" : cmd.asset.Trim(); string sideB = string.IsNullOrEmpty(cmd.value) ? "wolf" : cmd.value.Trim(); bool pairOnly = (cmd.expect ?? "").IndexOf("pair", StringComparison.OrdinalIgnoreCase) >= 0 || string.Equals(cmd.label, "pair", StringComparison.OrdinalIgnoreCase); if (pairOnly) { Actor a = scene?.FollowUnit ?? anchor; Actor b = scene?.RelatedUnit; if (b == null || !b.isAlive()) { // Nearest opposite-species unit. foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic()) { if (actor == null || actor == a || !actor.isAlive() || actor.asset == null) { continue; } if (actor.asset.id.Equals(sideB, StringComparison.OrdinalIgnoreCase)) { b = actor; break; } } } int wiredPair = 0; if (TrySetAttackTarget(a, b)) { wiredPair++; } if (TrySetAttackTarget(b, a)) { wiredPair++; } InterestDirector.HarnessMaintainCombatFocus(); bool okPair = wiredPair > 0; if (okPair) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, okPair, detail: $"pair wired={wiredPair} a={SafeName(a)} b={SafeName(b)} tip='{CameraDirector.LastWatchLabel}' focus={FocusLabel()}"); return; } var campA = new List(8); var campB = new List(8); Vector2 pos = anchor.current_position; const float r2 = 16f * 16f; foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic()) { if (actor == null || !actor.isAlive() || actor.asset == null) { continue; } float dx = actor.current_position.x - pos.x; float dy = actor.current_position.y - pos.y; if (dx * dx + dy * dy > r2) { continue; } string id = actor.asset.id ?? ""; if (id.Equals(sideA, StringComparison.OrdinalIgnoreCase)) { campA.Add(actor); } else if (id.Equals(sideB, StringComparison.OrdinalIgnoreCase)) { campB.Add(actor); } } if (campA.Count == 0 || campB.Count == 0) { _cmdFail++; Emit(cmd, ok: false, detail: $"empty camps a={campA.Count} b={campB.Count} near={SafeName(anchor)}"); return; } int wired = 0; for (int i = 0; i < campA.Count; i++) { if (TrySetAttackTarget(campA[i], campB[i % campB.Count])) { wired++; } } for (int i = 0; i < campB.Count; i++) { if (TrySetAttackTarget(campB[i], campA[i % campA.Count])) { wired++; } } // Enroll camps on the sticky scoreboard. AI often clears attack_target on pack // extras within a tick; roster membership still lets Duel → Mass escalate. if (scene != null && campA.Count > 0 && campB.Count > 0) { StampCombatCampsOntoScene(scene, sideA, sideB, campA, campB); } InterestDirector.HarnessMaintainCombatFocus(); bool ok = wired > 0; if (ok) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, ok, detail: $"wired={wired} a={campA.Count}:{sideA} b={campB.Count}:{sideB} sticky={scene?.CombatSideACount ?? 0}:{scene?.CombatSideBCount ?? 0} peak={scene?.CombatPeakParticipants ?? 0} tip='{CameraDirector.LastWatchLabel}' focus={FocusLabel()}"); } private static void StampCombatCampsOntoScene( InterestCandidate scene, string sideA, string sideB, List campA, List campB) { if (scene?.Sticky == null || campA == null || campB == null) { return; } LiveSceneStickyState sticky = scene.Sticky; sticky.Kind = EnsembleKind.Combat; sticky.Frame = EnsembleFrame.SpeciesVsSpecies; sticky.SideAKey = sideA; sticky.SideBKey = sideB; sticky.SideADisplay = LiveEnsemble.SpeciesDisplay(sideA); sticky.SideBDisplay = LiveEnsemble.SpeciesDisplay(sideB); sticky.SideAIds.Clear(); sticky.SideBIds.Clear(); for (int i = 0; i < campA.Count; i++) { long id = EventFeedUtil.SafeId(campA[i]); if (id != 0 && !sticky.SideAIds.Contains(id)) { sticky.SideAIds.Add(id); } } for (int i = 0; i < campB.Count; i++) { long id = EventFeedUtil.SafeId(campB[i]); if (id != 0 && !sticky.SideBIds.Contains(id)) { sticky.SideBIds.Add(id); } } sticky.SideACount = sticky.SideAIds.Count; sticky.SideBCount = sticky.SideBIds.Count; int total = sticky.SideACount + sticky.SideBCount; scene.ParticipantCount = Math.Max(scene.ParticipantCount, total); if (total > scene.CombatPeakParticipants) { scene.CombatPeakParticipants = total; } } /// /// Harness: zero one sticky combat camp while keeping PeakParticipants high /// (repro for Mass vs (0) mop-up labels). value = a|b (default b). /// private static void DoCombatWipeStickySide(HarnessCommand cmd) { InterestCandidate scene = InterestDirector.CurrentCandidate; if (scene?.Sticky == null || !scene.Sticky.HasOpposingSides) { _cmdFail++; Emit(cmd, ok: false, detail: "no sticky opposing combat sides"); return; } LiveSceneStickyState sticky = scene.Sticky; string which = (cmd.value ?? "b").Trim().ToLowerInvariant(); bool wipeB = which != "a" && which != "sidea" && which != "0"; int peakBefore = sticky.PeakParticipants; if (wipeB) { sticky.SideBIds.Clear(); sticky.SideBCount = 0; } else { sticky.SideAIds.Clear(); sticky.SideACount = 0; } // Keep peak Mass so the bug class (peak hold + empty camp) is exercised. sticky.PeakParticipants = Math.Max(peakBefore, Math.Max(8, sticky.TotalCount)); scene.ParticipantCount = sticky.TotalCount; scene.CombatSideACount = sticky.SideACount; scene.CombatSideBCount = sticky.SideBCount; scene.CombatPeakParticipants = sticky.PeakParticipants; string reason = StickyScoreboard.FormatReason(scene, scene.FollowUnit, scene.RelatedUnit); if (!string.IsNullOrEmpty(reason)) { scene.Label = reason; } StickyScoreboard.StabilizeScale(sticky, new LiveEnsemble { Kind = EnsembleKind.Combat, Frame = sticky.Frame, ParticipantCount = Math.Max(1, sticky.TotalCount), SideA = new EnsembleSide { Key = sticky.SideAKey, Display = sticky.SideADisplay, Count = sticky.SideACount }, SideB = new EnsembleSide { Key = sticky.SideBKey, Display = sticky.SideBDisplay, Count = sticky.SideBCount }, Focus = scene.FollowUnit, Related = scene.RelatedUnit }, Time.unscaledTime); string label = EventReason.Combat(new LiveEnsemble { Kind = EnsembleKind.Combat, Scale = sticky.HasPresentedScale ? sticky.PresentedScale : LiveEnsemble.ScaleForCount(Math.Max(1, sticky.TotalCount)), Frame = sticky.Frame, Focus = scene.FollowUnit, Related = scene.RelatedUnit, SideA = new EnsembleSide { Key = sticky.SideAKey, Display = sticky.SideADisplay, KingdomDisplay = sticky.SideAKingdom, Count = sticky.SideACount }, SideB = new EnsembleSide { Key = sticky.SideBKey, Display = sticky.SideBDisplay, KingdomDisplay = sticky.SideBKingdom, Count = sticky.SideBCount }, ParticipantCount = sticky.TotalCount }); if (!string.IsNullOrEmpty(label)) { scene.Label = label; CameraDirector.Watch(scene.ToInterestEvent()); } _cmdOk++; Emit( cmd, ok: true, detail: $"wiped={(wipeB ? "b" : "a")} sides={sticky.SideACount}/{sticky.SideBCount} peak={sticky.PeakParticipants} tip='{scene.Label}'"); } /// /// Harness: set one sticky camp's live count without clearing mop-up /// (value = a:N or b:N). Used to soak 0↔1 pack↔vs thrash. /// private static void DoCombatSetStickySideCount(HarnessCommand cmd) { InterestCandidate scene = InterestDirector.CurrentCandidate; if (scene?.Sticky == null || !scene.Sticky.HasOpposingSides) { _cmdFail++; Emit(cmd, ok: false, detail: "no sticky opposing combat sides"); return; } string raw = (cmd.value ?? "b:1").Trim().ToLowerInvariant(); string which = "b"; int count = 1; int colon = raw.IndexOf(':'); if (colon > 0) { which = raw.Substring(0, colon).Trim(); int.TryParse(raw.Substring(colon + 1).Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out count); } else if (int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out int onlyCount)) { count = onlyCount; } count = Math.Max(0, count); LiveSceneStickyState sticky = scene.Sticky; bool setB = which != "a" && which != "sidea" && which != "0"; if (setB) { sticky.SideBCount = count; if (count <= 0) { sticky.SideBIds.Clear(); } } else { sticky.SideACount = count; if (count <= 0) { sticky.SideAIds.Clear(); } } scene.CombatSideACount = sticky.SideACount; scene.CombatSideBCount = sticky.SideBCount; scene.ParticipantCount = sticky.TotalCount; StickyScoreboard.StabilizeScale( sticky, new LiveEnsemble { Kind = EnsembleKind.Combat, Frame = sticky.Frame, ParticipantCount = Math.Max(1, sticky.TotalCount), SideA = new EnsembleSide { Key = sticky.SideAKey, Display = sticky.SideADisplay, KingdomDisplay = sticky.SideAKingdom, Count = sticky.SideACount }, SideB = new EnsembleSide { Key = sticky.SideBKey, Display = sticky.SideBDisplay, KingdomDisplay = sticky.SideBKingdom, Count = sticky.SideBCount }, Focus = scene.FollowUnit, Related = scene.RelatedUnit }, Time.unscaledTime); string label = StickyScoreboard.FormatReason(scene, scene.FollowUnit, scene.RelatedUnit); if (!string.IsNullOrEmpty(label)) { scene.Label = label; CameraDirector.Watch(scene.ToInterestEvent()); } _cmdOk++; Emit( cmd, ok: true, detail: $"set={(setB ? "b" : "a")}:{count} sides={sticky.SideACount}/{sticky.SideBCount} mop={sticky.MopUpActive} tip='{scene.Label}'"); } /// Diagnostics: how many combat fighters LiveEnsemble sees near the scene focus. private static void DoCombatProbeFighters(HarnessCommand cmd) { InterestCandidate scene = InterestDirector.CurrentCandidate; Actor anchor = scene?.FollowUnit; if (anchor == null || !anchor.isAlive()) { anchor = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; } if (anchor == null || !anchor.isAlive()) { _cmdFail++; Emit(cmd, ok: false, detail: "no_anchor"); return; } float radius = ParseFloat(cmd.value, 18f); if (radius < 4f) { radius = 18f; } LiveEnsemble.TryBuildCombat( anchor.current_position, radius, out LiveEnsemble ensemble, priorParticipantIds: scene?.ParticipantIds, newcomerBonus: 8f); int n = ensemble != null ? ensemble.ParticipantCount : 0; string frame = ensemble != null ? ensemble.Frame.ToString() : "none"; string scale = ensemble != null ? ensemble.Scale.ToString() : "none"; bool sided = ensemble != null && LiveEnsemble.HasOpposingCollectiveSides(ensemble); bool focusFight = LiveEnsemble.IsCombatParticipant(anchor); bool relatedFight = scene?.RelatedUnit != null && LiveEnsemble.IsCombatParticipant(scene.RelatedUnit); _cmdOk++; Emit( cmd, ok: true, detail: $"n={n} r={radius:0.#} frame={frame} scale={scale} sided={sided} focusFight={focusFight} relatedFight={relatedFight} tip='{CameraDirector.LastWatchLabel}'"); } /// /// Kill the locked pair partner while leaving Follow fighting. First-partner lock must /// not chain-adopt the next attack_target into a new Duel tip. /// private static void DoCombatKillRelated(HarnessCommand cmd) { InterestCandidate scene = InterestDirector.CurrentCandidate; if (scene == null || scene.Completion != InterestCompletionKind.CombatActive) { _cmdFail++; Emit(cmd, ok: false, detail: "no_combat_scene"); return; } Actor partner = scene.RelatedUnit; if (partner == null || !partner.isAlive()) { partner = EventFeedUtil.FindUnitById(scene.PairPartnerId); } if (partner == null) { _cmdFail++; Emit(cmd, ok: false, detail: "no_partner"); return; } long partnerId = EventFeedUtil.SafeId(partner); string partnerName = SafeName(partner); try { if (partner.isAlive()) { partner.die(true, AttackType.Divine); } } catch (Exception ex) { _cmdFail++; Emit(cmd, ok: false, detail: $"die_failed={ex.Message}"); return; } bool dead = false; try { dead = partner == null || !partner.isAlive(); } catch { dead = true; } // Keep the durable partner id so maintain proves we do not chain-replace. if (partnerId != 0) { scene.PairPartnerId = partnerId; } // Drop dead RelatedUnit refs so later maintain / swap cannot touch destroyed actors. scene.RelatedUnit = null; scene.RelatedId = partnerId; Actor focus = scene.FollowUnit; if (focus == null && MoveCamera.hasFocusUnit()) { focus = MoveCamera._focus_unit; } ClearAttackTarget(focus); // Thin Duel tips only: immediately leave NamedPair wording naming the corpse. // Collective Mass/Battle tips stay for maintain / sticky framing. string priorTip = scene.Label ?? ""; bool thinDuel = priorTip.StartsWith("Duel", StringComparison.OrdinalIgnoreCase); if (dead && focus != null && thinDuel) { try { if (focus.isAlive()) { string holdTip = EventReason.Fight(focus, null); if (!string.IsNullOrEmpty(holdTip)) { scene.Label = holdTip; scene.FollowUnit = focus; scene.SubjectId = EventFeedUtil.SafeId(focus); CameraDirector.Watch(scene.ToInterestEvent()); } } } catch { // maintain path still owns the hold } } if (dead) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, dead, detail: $"partner={partnerName} id={partnerId} lock={scene.PairPartnerId} tip='{CameraDirector.LastWatchLabel}'"); } /// /// Spawn a distractor and point the combat Follow's attack_target at them while the /// locked pair partner stays alive. Maintain / director must not flip the Duel tip. /// private static void DoCombatSwapAttackTarget(HarnessCommand cmd) { try { InterestCandidate scene = InterestDirector.CurrentCandidate; if (scene == null || scene.Completion != InterestCompletionKind.CombatActive) { _cmdFail++; Emit(cmd, ok: false, detail: "no_combat_scene"); return; } Actor focus = scene.FollowUnit; if (focus == null) { focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; } try { if (focus == null || !focus.isAlive()) { focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; } } catch { focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; } bool focusOk = false; try { focusOk = focus != null && focus.isAlive(); } catch { focusOk = false; } if (!focusOk) { _cmdFail++; Emit(cmd, ok: false, detail: "no_focus"); return; } // Stale attack_target after partner death can NRE later Unity paths. ClearAttackTarget(focus); Actor partner = null; try { partner = scene.RelatedUnit; if (partner != null && !partner.isAlive()) { partner = null; } } catch { partner = null; } if (partner == null && scene.PairPartnerId != 0) { partner = LiveEnsemble.FindTrackedActor(scene.PairPartnerId); } string distractorAsset = !string.IsNullOrEmpty(cmd.asset) ? cmd.asset.Trim() : "wolf"; if (string.IsNullOrEmpty(cmd.asset) && partner != null) { try { if (partner.asset != null && !string.IsNullOrEmpty(partner.asset.id)) { distractorAsset = partner.asset.id; } } catch { distractorAsset = "wolf"; } } Vector3 near = default; try { near = focus.current_position; } catch { _cmdFail++; Emit(cmd, ok: false, detail: "focus_pos_failed"); return; } Actor distractor = SpawnAssetNear(distractorAsset, near, 3.5f); if (distractor == null) { _cmdFail++; Emit(cmd, ok: false, detail: "spawn_failed"); return; } try { if (!distractor.isAlive()) { _cmdFail++; Emit(cmd, ok: false, detail: "spawn_dead"); return; } } catch { _cmdFail++; Emit(cmd, ok: false, detail: "spawn_invalid"); return; } // Keep the locked partner alive so the hold must prefer them over the new target. TryHarnessInvincible(partner, 12f); TryHarnessInvincible(distractor, 8f); bool wired = TrySetAttackTarget(focus, distractor); TrySetAttackTarget(distractor, focus); // Peer noise without requiring a living RelatedUnit (post partner-death). try { InterestFeeds.OnActivityNote(focus, "fighting", hot: true); } catch (Exception ex) { // Wire alone still exercises the hold; do not fail the step on feed NRE. Emit( cmd, wired, detail: $"wired={wired} activity_note_err={ex.Message} focus={SafeName(focus)} distractor={SafeName(distractor)} tip='{CameraDirector.LastWatchLabel}'"); if (wired) { _cmdOk++; } else { _cmdFail++; } return; } if (wired) { _cmdOk++; } else { _cmdFail++; } Emit( cmd, wired, detail: $"focus={SafeName(focus)} partner={SafeName(partner)} distractor={SafeName(distractor)} tip='{CameraDirector.LastWatchLabel}'"); } catch (Exception ex) { _cmdFail++; Emit(cmd, ok: false, detail: $"swap_exception={ex.GetType().Name}:{ex.Message}"); } } private static void TryHarnessInvincible(Actor unit, float seconds) { if (unit == null) { return; } try { if (!unit.isAlive()) { return; } StatusGameApi.TryAddStatus(unit, "invincible", seconds); } catch { // Status API can NRE on edge actor states - wire alone is enough for holds. } } private static bool TrySetAttackTarget(Actor unit, Actor foe) { if (unit == null || foe == null || !unit.isAlive() || !foe.isAlive() || unit == foe) { return false; } try { var mi = typeof(Actor).GetMethod( "setAttackTarget", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic); if (mi != null) { mi.Invoke(unit, new object[] { foe }); } else { var prop = typeof(Actor).GetProperty( "attack_target", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic); prop?.SetValue(unit, foe, null); } return unit.has_attack_target; } catch { return false; } } private static Actor SpawnAssetNear(string assetId, Vector3 near, float jitter) { try { if (AssetManager.actor_library?.get(assetId) == null || World.world == null) { return null; } WorldTile tile = World.world.GetTile( Mathf.RoundToInt(near.x + jitter), Mathf.RoundToInt(near.y)); if (tile == null) { tile = World.world.GetTile( Mathf.RoundToInt(near.x), Mathf.RoundToInt(near.y)); } if (tile == null) { return null; } Actor spawned = World.world.units.spawnNewUnit( assetId, tile, pSpawnSound: false, pMiracleSpawn: true); if (spawned != null && spawned.isAlive()) { _lastSpawned = spawned; _lastSpawnedAssetId = assetId; } return spawned; } catch { return null; } } private static void DoSetSetting(HarnessCommand cmd) { string key = cmd.expect; if (string.IsNullOrEmpty(key)) { key = cmd.asset; } if (string.IsNullOrEmpty(key)) { _cmdFail++; Emit(cmd, ok: false, detail: "missing setting key (expect/asset)"); return; } bool ok; string detail; if (int.TryParse((cmd.value ?? "").Trim(), out int intVal) && (key == ModSettings.GravestoneMaxStacksId || key == ModSettings.GravestoneTtlSecondsId)) { ok = ModSettings.TrySetInt(key, intVal); detail = $"{key}={intVal}"; } else { bool val = ParseBool(cmd.value, defaultValue: true); ok = ModSettings.TrySetBool(key, val); detail = $"{key}={val}"; } if (ok) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: detail); } private static InterestCandidate HarnessNarrativeCandidate( string key, Actor subject, string label, float eventStrength) { long id = EventFeedUtil.SafeId(subject); return new InterestCandidate { Key = key ?? "", Source = "harness", Category = "Story", AssetId = key ?? "", Label = EventPresentability.EnsureSubjectLedLabel(subject, label ?? ""), FollowUnit = subject, SubjectId = id, Position = subject != null ? subject.current_position : Vector3.zero, EventStrength = eventStrength, CharacterSignificance = 5f, VisualConfidence = 0.8f, Novelty = 1f, Completion = InterestCompletionKind.FixedDwell, MinWatch = 1f, MaxWatch = 15f, CreatedAt = Time.unscaledTime, LastSeenAt = Time.unscaledTime, ExpiresAt = Time.unscaledTime + 30f, Resumable = false }; } private static void DoAssert(HarnessCommand cmd) { string expect = (cmd.expect ?? "").Trim().ToLowerInvariant(); if (string.IsNullOrEmpty(expect)) { expect = (cmd.value ?? "").Trim().ToLowerInvariant(); } bool pass; string detail; switch (expect) { case "inspect_cleared": { pass = !SpectatorMode.Active && !Chronicle.HudVisible && !WatchCaption.Visible && !ChronicleHud.GraveStackScopeActive && !InspectUi.GraveSessionActive; detail = $"idle={SpectatorMode.Active} lore={Chronicle.HudVisible} dossier={WatchCaption.Visible} scope={ChronicleHud.GraveStackScopeActive} graveSession={InspectUi.GraveSessionActive}"; break; } case "idle": case "spectator": { bool want = ParseBool(cmd.value, defaultValue: true); // When expect is the whole assert name and value holds bool: if (expect == "idle" || expect == "spectator") { want = ParseBool(string.IsNullOrEmpty(cmd.value) ? "true" : cmd.value, true); } pass = SpectatorMode.Active == want; detail = $"idle={SpectatorMode.Active} want={want}"; break; } case "focus": { bool want = ParseBool(cmd.value, defaultValue: true); bool has = MoveCamera.hasFocusUnit(); pass = has == want; detail = $"focus={has} want={want}"; break; } case "power_bar": case "bar": { bool want = ParseBool(cmd.value, defaultValue: false); bool bar = CanvasMain.isBottomBarShowing(); pass = bar == want; detail = $"powerBar={bar} want={want}"; break; } case "health": { bool idle = SpectatorMode.Active; bool focus = MoveCamera.hasFocusUnit(); bool bar = CanvasMain.isBottomBarShowing(); pass = idle && focus && !bar && StateProbe.BadEventCount == 0; detail = $"idle={idle} focus={focus} powerBar={bar} bad={StateProbe.BadEventCount}"; break; } case "tip_asset": { string want = (cmd.value ?? cmd.asset ?? "").Trim(); string tipAsset = CameraDirector.LastWatchAssetId ?? ""; pass = !string.IsNullOrEmpty(want) && tipAsset.IndexOf(want, StringComparison.OrdinalIgnoreCase) >= 0; detail = $"tipAsset='{tipAsset}' want='{want}'"; break; } case "tip_not_partner": { // Remembered bystander must not be named as the fallen / sought victim. // Subject can still be the partner ("Petelor stands over the fallen" is fine). string partnerName = SafeName(_happinessPartner); string tip = CameraDirector.LastWatchLabel ?? ""; bool namedAsOther = !string.IsNullOrEmpty(partnerName) && (tip.IndexOf("stands over " + partnerName, StringComparison.OrdinalIgnoreCase) >= 0 || tip.IndexOf("mourns " + partnerName, StringComparison.OrdinalIgnoreCase) >= 0 || tip.IndexOf("seeks " + partnerName, StringComparison.OrdinalIgnoreCase) >= 0 || tip.IndexOf("stays beside " + partnerName, StringComparison.OrdinalIgnoreCase) >= 0); pass = !namedAsOther; detail = $"tip='{tip}' partner='{partnerName}' namedAsOther={namedAsOther}"; break; } case "story_phase": { string want = (cmd.value ?? "").Trim(); StoryArc arc = StoryPlanner.Active; string have = arc != null ? arc.Phase.ToString() : "None"; pass = !string.IsNullOrEmpty(want) && string.Equals(have, want, StringComparison.OrdinalIgnoreCase); detail = $"storyPhase={have} want={want} kind={arc?.Kind} hard={arc?.HardHold} board={StoryPlanner.BoardCount}"; break; } case "story_board_count": { int want = ParseInt(cmd.value, 0); int have = StoryPlanner.BoardCount; string cmp = (cmd.label ?? "").Trim().ToLowerInvariant(); if (cmp == "min") { pass = have >= want; } else if (cmp == "max") { pass = have <= want; } else { pass = have == want; } detail = $"boardCount={have} want={want} cmp={cmp}"; break; } case "story_parked_count": { int want = ParseInt(cmd.value, 0); var board = new System.Collections.Generic.List(4); StoryPlanner.CopyBoard(board); int parked = 0; for (int i = 0; i < board.Count; i++) { if (board[i] != null && board[i].IsParked) { parked++; } } string cmp = (cmd.label ?? "").Trim().ToLowerInvariant(); pass = cmp == "min" ? parked >= want : parked == want; detail = $"parked={parked} want={want} board={board.Count}"; break; } case "browse_paused": { bool want = ParseBool(cmd.value, defaultValue: true); bool have = SpectatorMode.BrowsePaused; pass = have == want; detail = $"browsePaused={have} want={want} idle={SpectatorMode.Active}"; break; } case "saga_roster_count": case "story_rail_count": { LifeSagaRoster.Tick(Time.unscaledTime); LifeSagaRail.Refresh(); int want = ParseInt(cmd.value, 0); int have = LifeSagaRoster.Count; int shown = LifeSagaRail.LastShownCount; string cmp = (cmd.label ?? "").Trim().ToLowerInvariant(); string expectKey = (cmd.expect ?? "").Trim().ToLowerInvariant(); int probe = expectKey == "story_rail_count" ? shown : have; pass = cmp == "min" ? probe >= want : probe == want; detail = $"roster={have} railShown={shown} want={want} cmp={cmp} visible={LifeSagaRail.Visible}"; break; } case "saga_overview_tip": case "story_rail_tip": { string have = SampleRailTip(); string any = (cmd.value ?? "").Trim(); pass = false; if (!string.IsNullOrEmpty(have) && !string.IsNullOrEmpty(any)) { string[] parts = any.Split('|'); for (int i = 0; i < parts.Length; i++) { string needle = (parts[i] ?? "").Trim(); if (needle.Length > 0 && have.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0) { pass = true; break; } } } detail = $"sagaTip='{have}' any='{any}' pass={pass}"; break; } case "saga_overview_tip_not": case "story_rail_tip_not": { string have = SampleRailTip(); string any = (cmd.value ?? "").Trim(); pass = true; if (!string.IsNullOrEmpty(have) && !string.IsNullOrEmpty(any)) { string[] parts = any.Split('|'); for (int i = 0; i < parts.Length; i++) { string needle = (parts[i] ?? "").Trim(); if (needle.Length > 0 && have.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0) { pass = false; break; } } } detail = $"railTip='{have}' not_any='{any}' pass={pass}"; break; } case "crisis_active": { bool want = ParseBool(cmd.value, defaultValue: true); bool have = StoryPlanner.CrisisActive; pass = have == want; CrisisChapter crisis = StoryPlanner.Crisis; detail = $"crisisActive={have} want={want} kind={crisis?.Kind} phase={crisis?.Phase}"; break; } case "soft_fill_quiet": { bool want = ParseBool(cmd.value, defaultValue: true); bool have = StoryPlanner.SoftFillQuietActive; pass = have == want; detail = $"softFillQuiet={have} want={want}"; break; } case "crisis_kind": { string want = (cmd.value ?? "").Trim(); CrisisChapter crisis = StoryPlanner.Crisis; string have = crisis != null ? crisis.Kind.ToString() : "None"; pass = !string.IsNullOrEmpty(want) && string.Equals(have, want, StringComparison.OrdinalIgnoreCase); detail = $"crisisKind={have} want={want} active={StoryPlanner.CrisisActive}"; break; } case "story_spine": { string want = (cmd.value ?? "").Trim(); // HUD display only - do not fall back to FormatSpineLabel (Climax may be suppressed). string have = WatchCaption.LastStorySpine ?? ""; string formatted = StoryPlanner.FormatSpineLabel() ?? ""; bool wantEmpty = string.IsNullOrEmpty(want) || string.Equals(want, "empty", StringComparison.OrdinalIgnoreCase) || string.Equals(want, "none", StringComparison.OrdinalIgnoreCase); if (wantEmpty) { pass = string.IsNullOrEmpty(have); } else if (want.IndexOf('|') >= 0) { // Alternatives: "Battle · Climax|Mass · Climax|Skirmish · Climax" pass = false; string[] alts = want.Split('|'); for (int i = 0; i < alts.Length; i++) { string alt = (alts[i] ?? "").Trim(); if (!string.IsNullOrEmpty(alt) && have.IndexOf(alt, StringComparison.OrdinalIgnoreCase) >= 0) { pass = true; break; } } } else { pass = have.IndexOf(want, StringComparison.OrdinalIgnoreCase) >= 0; } detail = $"spine='{have}' want='{want}' formatted='{formatted}'"; break; } case "story_spine_formatted": { string want = (cmd.value ?? "empty").Trim(); string formatted = StoryPlanner.FormatSpineLabel() ?? ""; bool wantEmpty = string.IsNullOrEmpty(want) || string.Equals(want, "empty", StringComparison.OrdinalIgnoreCase) || string.Equals(want, "none", StringComparison.OrdinalIgnoreCase); pass = wantEmpty ? string.IsNullOrEmpty(formatted) : formatted.IndexOf(want, StringComparison.OrdinalIgnoreCase) >= 0; detail = $"formatted='{formatted}' want='{want}' context='{WatchCaption.LastContextLine}'"; break; } case "story_spine_not_formatted": { string formatted = StoryPlanner.FormatSpineLabel() ?? ""; string context = WatchCaption.LastStorySpine ?? ""; pass = string.IsNullOrEmpty(formatted) || context.IndexOf(formatted, StringComparison.OrdinalIgnoreCase) < 0; detail = $"formatted='{formatted}' context='{context}' pass={pass}"; break; } case "story_hold_margin": { // value: "0" / "none" → expect no raised hold; "gt0" / "held" → expect hold > 0 string want = (cmd.value ?? "gt0").Trim(); InterestCandidate cur = InterestDirector.CurrentCandidate; InterestCandidate probe = new InterestCandidate { Key = "harness:story_hold_probe", SubjectId = 999999001, EventStrength = 40f, LeadKind = InterestLeadKind.EventLed, Completion = InterestCompletionKind.FixedDwell, Label = "HoldProbe" }; InterestScoring.RecalcTotal(probe); float margin = StoryPlanner.ArcHoldMargin(cur, probe); bool wantHeld = string.Equals(want, "gt0", StringComparison.OrdinalIgnoreCase) || string.Equals(want, "held", StringComparison.OrdinalIgnoreCase) || string.Equals(want, "true", StringComparison.OrdinalIgnoreCase); bool wantNone = string.Equals(want, "0", StringComparison.OrdinalIgnoreCase) || string.Equals(want, "none", StringComparison.OrdinalIgnoreCase) || string.Equals(want, "false", StringComparison.OrdinalIgnoreCase); if (wantHeld) { pass = margin > 0f; } else if (wantNone) { pass = margin <= 0f; } else if (float.TryParse(want, NumberStyles.Float, CultureInfo.InvariantCulture, out float wantF)) { pass = Mathf.Abs(margin - wantF) < 0.5f; } else { pass = false; } detail = $"storyHold={margin:0.#} want={want} phase={StoryPlanner.Active?.Phase.ToString() ?? "None"} " + $"belongs={StoryPlanner.BelongsToActiveStoryBeat(cur)}"; break; } case "beat_cooled": { // value = happiness/asset needle; builds a probe on focus unit. string needle = (cmd.value ?? "just_had_child").Trim(); Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; long id = EventFeedUtil.SafeId(focus); InterestCandidate probe = new InterestCandidate { Key = "harness:beat_cool:" + needle, SubjectId = id, FollowUnit = focus, HappinessEffectId = needle, AssetId = needle, LeadKind = InterestLeadKind.EventLed, Completion = InterestCompletionKind.FixedDwell, EventStrength = 80f, Label = needle }; // Map decision/building/status needles onto the same chapter fields as injects. ApplyHarnessRelationshipAssetHint(probe, needle); pass = InterestVariety.IsBeatCooled(probe); detail = $"beatCooled={pass} needle='{needle}' focus={SafeName(focus)} id={id}"; break; } case "ledger_has_focus": case "saga_has_focus": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; long id = EventFeedUtil.SafeId(focus); LifeSagaRoster.Tick(Time.unscaledTime); bool want = ParseBool(cmd.value, defaultValue: true); bool have = id != 0 && LifeSagaRoster.IsMc(id); pass = have == want; detail = $"saga focus={SafeName(focus)} id={id} on={have} want={want} prefer={LifeSagaRoster.IsPrefer(id)}"; break; } case "saga_prefer_focus_on": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; long id = EventFeedUtil.SafeId(focus); bool want = ParseBool(cmd.value, defaultValue: true); bool have = id != 0 && LifeSagaRoster.IsPrefer(id); pass = have == want; detail = $"prefer focus={SafeName(focus)} id={id} have={have} want={want}"; break; } case "saga_rail_active_highlight": { LifeSagaRail.Refresh(); bool want = ParseBool(cmd.value, defaultValue: true); pass = LifeSagaRail.LastActiveHighlight == want; detail = $"activeLit={LifeSagaRail.LastActiveHighlight} want={want} preferPip={LifeSagaRail.LastPreferPipVisible}"; break; } case "saga_rail_prefer_pip": { LifeSagaRail.Refresh(); bool want = ParseBool(cmd.value, defaultValue: true); pass = LifeSagaRail.LastPreferPipVisible == want; detail = $"preferPip={LifeSagaRail.LastPreferPipVisible} want={want} activeLit={LifeSagaRail.LastActiveHighlight}"; break; } case "saga_rail_involved_count": { LifeSagaRail.Refresh(); int want = 1; if (!string.IsNullOrEmpty(cmd.value) && int.TryParse(cmd.value.Trim(), out int parsed)) { want = parsed; } pass = LifeSagaRail.LastInvolvedHighlightCount >= want; detail = $"involvedLit={LifeSagaRail.LastInvolvedHighlightCount} want>={want} active={LifeSagaRail.LastActiveHighlight}"; break; } case "saga_rail_cached_faces": { int want = ParseInt(cmd.value, 1); bool atLeast = string.Equals(cmd.label, "min", StringComparison.OrdinalIgnoreCase); int have = LifeSagaRail.CachedFaceCount; pass = atLeast ? have >= want : have == want; detail = $"cachedFaces={have} want={want} mode={(atLeast ? "min" : "exact")} shown={LifeSagaRail.LastShownCount}"; break; } case "saga_soft_bias_rank": { InterestCandidate cur = InterestDirector.CurrentCandidate; int want = ParseInt(cmd.value, 3); int have = LifeSagaRoster.SoftBiasRank(cur); string cmp = (cmd.label ?? "").Trim().ToLowerInvariant(); pass = cmp == "min" ? have >= want : have == want; detail = $"softBias={have} want={want} cmp={cmp} touched={LifeSagaRoster.CountTouchedMcs(cur)} tip={cur?.Label}"; break; } case "saga_score_bonus_min": { InterestCandidate cur = InterestDirector.CurrentCandidate; float want = ParseFloat(cmd.value, 20f); float have = LifeSagaRoster.ScoreBonus(cur); pass = have + 0.05f >= want; detail = $"sagaBonus={have:0.#} want>={want:0.#} tip={cur?.Label}"; break; } case "saga_rival_focus": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; long id = EventFeedUtil.SafeId(focus); bool want = ParseBool(cmd.value, defaultValue: true); bool have = LifeSagaMemory.TryGetEarnedRival(id, out LifeSagaFact rival) && rival != null; pass = have == want; detail = $"rival focus={SafeName(focus)} have={have} want={want} note={rival?.Note}"; break; } case "saga_antagonist_partner": { Actor partner = _happinessPartner; bool want = ParseBool(cmd.value, defaultValue: true); bool have = LifeSagaRoster.TryGetRecurringAntagonist( partner, out long anchor, out LifeSagaFact evidence); pass = have == want; detail = $"antagonist partner={SafeName(partner)} have={have} want={want} anchor={anchor} evidence={evidence?.Note}"; break; } case "saga_crossover_focus": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; long id = EventFeedUtil.SafeId(focus); bool want = ParseBool(cmd.value, defaultValue: true); bool have = false; var facts = LifeSagaMemory.FactsFor(id); for (int i = 0; i < facts.Count; i++) { if (facts[i] != null && facts[i].McCrossover) { have = true; break; } } pass = have == want; detail = $"crossover focus={SafeName(focus)} have={have} want={want} facts={facts.Count}"; break; } case "saga_combat_weight_beats_partner": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; Actor partner = _happinessPartner; float wf = WorldActivityScanner.CombatFocusWeight(focus); float wp = WorldActivityScanner.CombatFocusWeight(partner); pass = focus != null && partner != null && wf > wp; detail = $"focusW={wf:0.#} partnerW={wp:0.#} focus={SafeName(focus)} partner={SafeName(partner)}"; break; } case "saga_snapshot_layout": { int rows = LifeSagaRail.LastHoverRowCount; float height = LifeSagaRail.LastHoverHeight; pass = LifeSagaViewController.EffectiveTab == LifeSagaViewController.Tab.Saga && Mathf.Abs(height - LifeSagaPanel.BodyHeightFixed) < 0.5f; detail = $"snapshot rows={rows} height={height:0.0} tab={LifeSagaViewController.EffectiveTab} min={LifeSagaPanel.BodyHeightMin} max={LifeSagaPanel.BodyHeightMax}"; break; } case "saga_tab_selected": { string want = (cmd.value ?? "dossier").Trim().ToLowerInvariant(); string have = LifeSagaViewController.SelectedTab == LifeSagaViewController.Tab.Saga ? "saga" : "dossier"; pass = have == want || (want.StartsWith("s") && have == "saga") || (want.StartsWith("d") && have == "dossier"); detail = $"selected={have} want={want}"; break; } case "saga_tab_effective": { string want = (cmd.value ?? "dossier").Trim().ToLowerInvariant(); string have = LifeSagaViewController.EffectiveTab == LifeSagaViewController.Tab.Saga ? "saga" : "dossier"; pass = have == want || (want.StartsWith("s") && have == "saga") || (want.StartsWith("d") && have == "dossier"); detail = $"effective={have} want={want} hover={LifeSagaViewController.HoverPreviewId}"; break; } case "saga_hover_preview": { bool want = ParseBool(cmd.value, defaultValue: true); bool have = LifeSagaViewController.IsHoverPreview; pass = have == want; detail = $"hoverPreview={have} want={want} id={LifeSagaViewController.HoverPreviewId}"; break; } case "saga_read_pause": { bool want = ParseBool(cmd.value, defaultValue: true); bool have = InterestDirector.ReadPauseActive; pass = have == want; detail = $"readPause={have} want={want}"; break; } case "saga_panel_bound": { LifeSagaViewModel model = LifeSagaViewController.BuildEffectivePresentation(); string shownName = WatchCaption.VisibleNametagText; string expectedName = (model?.Name ?? "").Trim(); if (expectedName.StartsWith("★ ", StringComparison.Ordinal)) { expectedName = expectedName.Substring(2).TrimStart(); } pass = model != null && model.UnitId != 0 && !string.IsNullOrEmpty(model.Name) && LifeSagaPanel.Visible && shownName.IndexOf(expectedName, StringComparison.Ordinal) >= 0; detail = $"boundId={model?.UnitId ?? 0} name='{model?.Name}' shown='{shownName}' visible={LifeSagaPanel.Visible} lens={model?.PrimaryLens}"; break; } case "saga_dossier_chrome_restored": { bool onDossier = LifeSagaViewController.EffectiveTab == LifeSagaViewController.Tab.Dossier && !LifeSagaViewController.IsHoverPreview; bool sagaHidden = !LifeSagaPanel.Visible; bool chrome = WatchCaption.HarnessDossierChromeRestored(); string expectedIdentity = WatchCaption.LastHeadline ?? ""; if (expectedIdentity.Length > 48) { expectedIdentity = CaptionComposer.TruncateIdentity(expectedIdentity, 48); } string shownIdentity = WatchCaption.VisibleNametagText; bool identityRestored = string.Equals(shownIdentity, expectedIdentity, StringComparison.Ordinal); pass = onDossier && sagaHidden && chrome && identityRestored; detail = $"onDossier={onDossier} sagaHidden={sagaHidden} chrome={chrome} identityRestored={identityRestored} shown='{shownIdentity}' expected='{expectedIdentity}' reason='{WatchCaption.LastDetail}'"; break; } case "saga_panel_lens": { LifeSagaViewModel model = LifeSagaViewController.BuildEffectivePresentation(); bool want = ParseBool(cmd.value, defaultValue: true); bool have = model != null && !string.IsNullOrEmpty(model.LensEyebrow); pass = have == want; detail = $"lensEyebrow='{model?.LensEyebrow}' want={want} primary={model?.PrimaryLens}"; break; } case "saga_panel_empty": { bool want = ParseBool(cmd.value, defaultValue: true); bool have = LifeSagaViewController.EffectiveTab == LifeSagaViewController.Tab.Saga && LifeSagaPanel.Visible && LifeSagaPanel.BoundUnitId == 0; pass = have == want; detail = $"empty={have} want={want} bound={LifeSagaPanel.BoundUnitId} effective={LifeSagaViewController.EffectiveSagaId}"; break; } case "chronicle_visible": { bool want = ParseBool(cmd.value, defaultValue: true); bool have = Chronicle.HudVisible; pass = have == want; detail = $"loreVisible={have} want={want}"; break; } case "saga_panel_fixed_size": { // Locked rail width + exact fixed-height Saga body. Vector2 size = WatchCaption.LastPanelSize; float bodyH = LifeSagaPanel.BodyHeight; float bodyW = LifeSagaPanel.BodyWidthPx; bool onSaga = LifeSagaViewController.EffectiveTab == LifeSagaViewController.Tab.Saga; bool widthOk = Mathf.Abs(bodyW - LifeSagaPanel.BodyWidthFixed) < 0.5f; bool heightOk = Mathf.Abs(bodyH - LifeSagaPanel.BodyHeightFixed) < 0.5f; bool compact = widthOk && heightOk; if (onSaga) { compact = compact && LifeSagaPanel.Visible && Mathf.Abs(size.y - WatchCaption.SagaHoverHeightFixed) < 0.5f && size.x <= 260f && size.x >= bodyW - 0.5f; } else { compact = size.x <= 260f && size.y <= 280f && size.y >= 40f; } pass = compact; detail = $"size={size} body={bodyW:0.#}x{bodyH:0.#} fixedW={LifeSagaPanel.BodyWidthFixed} outerH={WatchCaption.SagaHoverHeightFixed:0.#} onSaga={onSaga} compact={compact} dbg={LifeSagaPanel.LastLayoutDebug}"; break; } case "caption_mc_identity": { string idLine = WatchCaption.LastHeadline ?? ""; pass = idLine.IndexOf(" · ", StringComparison.Ordinal) >= 0 && (idLine.IndexOf("King", StringComparison.OrdinalIgnoreCase) >= 0 || idLine.IndexOf("Leader", StringComparison.OrdinalIgnoreCase) >= 0 || idLine.IndexOf("Alpha", StringComparison.OrdinalIgnoreCase) >= 0 || idLine.IndexOf("Chief", StringComparison.OrdinalIgnoreCase) >= 0 || idLine.IndexOf("Captain", StringComparison.OrdinalIgnoreCase) >= 0 || idLine.IndexOf("Founder", StringComparison.OrdinalIgnoreCase) >= 0 || idLine.IndexOf("Plotter", StringComparison.OrdinalIgnoreCase) >= 0); detail = $"identity='{idLine}' pass={pass}"; break; } case "caption_mc_identity_or_species": { string idLine = WatchCaption.LastHeadline ?? ""; pass = !string.IsNullOrEmpty(idLine) && idLine.IndexOf("A life in motion", StringComparison.OrdinalIgnoreCase) < 0; detail = $"identity='{idLine}' pass={pass}"; break; } case "caption_nobody_species": { string idLine = WatchCaption.LastHeadline ?? ""; bool hasTitleSep = idLine.IndexOf(" · ", StringComparison.Ordinal) >= 0 && (idLine.IndexOf("King", StringComparison.OrdinalIgnoreCase) >= 0 || idLine.IndexOf("Pack Alpha", StringComparison.OrdinalIgnoreCase) >= 0); pass = !string.IsNullOrEmpty(idLine) && !hasTitleSep; detail = $"identity='{idLine}' pass={pass}"; break; } case "caption_identity_not_in_label": { InterestCandidate cur = InterestDirector.CurrentCandidate; string label = cur != null ? (cur.Label ?? "") : ""; pass = label.IndexOf(" · King", StringComparison.OrdinalIgnoreCase) < 0 && label.IndexOf("Pack Alpha", StringComparison.OrdinalIgnoreCase) < 0 && !label.StartsWith("King of", StringComparison.OrdinalIgnoreCase); detail = $"label='{label}' identity='{WatchCaption.LastHeadline}' pass={pass}"; break; } case "caption_prose_no_jargon": { long id = EventFeedUtil.SafeId(MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null); string tip = LifeSagaPresentation.BuildPlainText(id) ?? ""; string beat = WatchCaption.LastBeatLine ?? ""; string ctx = WatchCaption.LastContextLine ?? ""; string all = tip + "\n" + beat + "\n" + ctx + "\n" + (WatchCaption.LastHeadline ?? ""); pass = !SagaProse.ContainsBannedPhrase(all); string sample = all.Replace('\n', ' '); if (sample.Length > 120) { sample = sample.Substring(0, 120); } detail = $"banned={!pass} sample='{sample}'"; break; } case "caption_prose_casing_cast": { long id = EventFeedUtil.SafeId(MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null); LifeSagaViewModel model = LifeSagaPresentation.Build(id); string any = (cmd.value ?? "Lover").Trim(); pass = false; if (model != null) { string[] parts = any.Split('|'); for (int i = 0; i < model.Cast.Count && !pass; i++) { string rel = model.Cast[i]?.Relation ?? ""; for (int p = 0; p < parts.Length; p++) { string want = (parts[p] ?? "").Trim(); if (want.Length > 0 && string.Equals(rel, want, StringComparison.Ordinal)) { pass = true; break; } } } } detail = $"wantExact='{any}' pass={pass}"; break; } case "caption_context_stake": { string ctx = WatchCaption.LastContextLine ?? ""; pass = !string.IsNullOrEmpty(ctx) && (ctx.IndexOf("war", StringComparison.OrdinalIgnoreCase) >= 0 || ctx.IndexOf("Still", StringComparison.OrdinalIgnoreCase) >= 0 || ctx.IndexOf("Feud", StringComparison.OrdinalIgnoreCase) >= 0 || ctx.IndexOf("tangled", StringComparison.OrdinalIgnoreCase) >= 0 || ctx.IndexOf("Bound", StringComparison.OrdinalIgnoreCase) >= 0 || ctx.IndexOf("Rules", StringComparison.OrdinalIgnoreCase) >= 0); bool looksLikeSpine = ctx.IndexOf(" · ", StringComparison.Ordinal) >= 0 && (ctx.StartsWith("Duel", StringComparison.OrdinalIgnoreCase) || ctx.StartsWith("War front", StringComparison.OrdinalIgnoreCase) || ctx.StartsWith("Love", StringComparison.OrdinalIgnoreCase) || ctx.StartsWith("Grief", StringComparison.OrdinalIgnoreCase) || ctx.StartsWith("Plot", StringComparison.OrdinalIgnoreCase) || ctx.StartsWith("Mass", StringComparison.OrdinalIgnoreCase) || ctx.StartsWith("Battle", StringComparison.OrdinalIgnoreCase)); if (looksLikeSpine && ctx.IndexOf("Still", StringComparison.OrdinalIgnoreCase) < 0) { pass = false; } detail = $"context='{ctx}' pass={pass}"; break; } case "caption_connection_present": { string connection = WatchCaption.LastConnectionLine ?? ""; pass = !string.IsNullOrEmpty(connection); detail = "connection='" + connection + "' pass=" + pass; break; } case "caption_connection_anchor": { string connection = WatchCaption.LastConnectionLine ?? ""; CameraEligibilityModel eligibility = CameraEligibility.Classify(InterestDirector.CurrentCandidate); pass = !string.IsNullOrEmpty(connection) && eligibility.AnchorMcId != 0 && connection == (eligibility.ConnectionLine ?? ""); detail = "connection='" + connection + "' anchor=" + eligibility.AnchorMcId + " kind=" + eligibility.ConnectionKind + " expected='" + eligibility.ConnectionLine + "' pass=" + pass; break; } case "camera_exceptional_policy": { pass = CameraEligibility.HarnessProbeExceptionalPolicy(out detail); break; } case "saga_civilized_no_pack_language": { Actor actor = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : _lastSpawned; pass = LifeSagaPresentation.HarnessProbeCivilizedTerminology(actor, out detail); break; } case "caption_context_omits": { // Soft tip hitch: Context must not show banned biography fragments. string ctx = WatchCaption.LastContextLine ?? ""; string banned = (cmd.value ?? "Slew|Became|Founded").Trim(); pass = true; string[] parts = banned.Split('|'); for (int i = 0; i < parts.Length; i++) { string want = (parts[i] ?? "").Trim(); if (want.Length > 0 && ctx.IndexOf(want, StringComparison.OrdinalIgnoreCase) >= 0) { pass = false; break; } } detail = $"context='{ctx}' banned='{banned}' pass={pass}"; break; } case "saga_panel_cast_first": { bool panel = LifeSagaPanel.Visible && LifeSagaPanel.BoundUnitId != 0; string dbg = LifeSagaPanel.LastLayoutDebug ?? ""; bool castFirst = dbg.IndexOf("castFirst=1", StringComparison.Ordinal) >= 0; bool ownsAvatar = LifeSagaPanel.OwnsAvatar; bool avatarVisible = DossierAvatar.ShowingLiveAvatar || DossierAvatar.ShowingSpeciesFallback; pass = panel && castFirst && ownsAvatar && avatarVisible; detail = $"panel={panel} ownsAvatar={ownsAvatar} avatarVisible={avatarVisible} live={DossierAvatar.ShowingLiveAvatar} dbg='{dbg}' pass={pass}"; break; } case "saga_panel_cast_relations": { int want = ParseInt(cmd.value, 1); int have = LifeSagaPanel.LastVisibleRelationCount; pass = LifeSagaPanel.Visible && have >= want; detail = $"relationLabels={have} wantMin={want} visible={LifeSagaPanel.Visible}"; break; } case "caption_hover_keeps_beat": { bool hover = LifeSagaViewController.IsHoverPreview; bool panel = LifeSagaPanel.Visible && LifeSagaPanel.BoundUnitId != 0; long focusId = EventFeedUtil.SafeId(MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null); LifeSagaViewModel hoverModel = LifeSagaViewController.BuildEffectivePresentation(); string identity = WatchCaption.VisibleNametagText; string hoverName = (hoverModel?.Name ?? "").Replace("★ ", "").Trim(); bool identityOk = !string.IsNullOrEmpty(hoverName) && identity.IndexOf(hoverName, StringComparison.Ordinal) >= 0; string beat = WatchCaption.VisibleBeatLine ?? ""; bool beatHidden = string.IsNullOrEmpty(beat); pass = hover && panel && identityOk && focusId != 0 && LifeSagaPanel.BoundUnitId == LifeSagaViewController.HoverPreviewId && beatHidden; detail = $"hover={hover} panelBound={LifeSagaPanel.BoundUnitId} hoverId={LifeSagaViewController.HoverPreviewId} identity='{identity}' hoverName='{hoverName}' beforeBeat='{_captionBeatAnchor}' beat='{beat}' beatHidden={beatHidden} pass={pass}"; break; } case "saga_memory_has_focus": { long id = EventFeedUtil.SafeId(MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null); bool want = ParseBool(cmd.value, defaultValue: true); bool have = LifeSagaMemory.HasFacts(id); pass = have == want; detail = $"id={id} hasFacts={have} want={want} lives={LifeSagaMemory.LifeCount}"; break; } case "saga_memory_fact_focus": { long id = EventFeedUtil.SafeId(MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null); string want = (cmd.value ?? "").Trim(); pass = false; var facts = LifeSagaMemory.FactsFor(id); for (int i = 0; i < facts.Count; i++) { if (facts[i] != null && facts[i].Kind.ToString().IndexOf(want, StringComparison.OrdinalIgnoreCase) >= 0) { pass = true; break; } } detail = $"id={id} wantKind={want} facts={facts.Count} pass={pass}"; break; } case "saga_memory_fact_other": { long id = EventFeedUtil.SafeId(MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null); long partnerId = EventFeedUtil.SafeId(_happinessPartner); string wantKind = (cmd.value ?? "WarJoin").Trim(); string mode = (cmd.label ?? "").Trim().ToLowerInvariant(); bool wantNonzero = mode == "nonzero" || mode == "any" || partnerId == 0 || partnerId == id; pass = false; var facts = LifeSagaMemory.FactsFor(id); for (int i = 0; i < facts.Count; i++) { LifeSagaFact f = facts[i]; if (f == null) { continue; } if (f.Kind.ToString().IndexOf(wantKind, StringComparison.OrdinalIgnoreCase) < 0) { continue; } if (wantNonzero) { if (f.OtherId != 0 && f.OtherId != id) { pass = true; break; } } else if (f.OtherId == partnerId) { pass = true; break; } } detail = $"id={id} partner={partnerId} wantKind={wantKind} mode={(wantNonzero ? "nonzero" : "partner")} pass={pass}"; break; } case "saga_memory_fact_resolved": { long id = EventFeedUtil.SafeId(MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null); string wantKind = (cmd.value ?? "War").Trim(); bool wantResolved = ParseBool(cmd.label, defaultValue: true); pass = false; var facts = LifeSagaMemory.FactsFor(id); for (int i = 0; i < facts.Count; i++) { LifeSagaFact f = facts[i]; if (f == null) { continue; } if (f.Kind.ToString().IndexOf(wantKind, StringComparison.OrdinalIgnoreCase) < 0 && (f.WarKey ?? "").IndexOf(wantKind, StringComparison.OrdinalIgnoreCase) < 0 && (f.PlotKey ?? "").IndexOf(wantKind, StringComparison.OrdinalIgnoreCase) < 0) { continue; } pass = f.Resolved == wantResolved; if (pass) { break; } } detail = $"id={id} wantKind={wantKind} wantResolved={wantResolved} pass={pass}"; break; } case "saga_stake_contains": { long id = EventFeedUtil.SafeId(MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null); if (id == 0) { var board = new System.Collections.Generic.List(LifeSagaRoster.Cap); LifeSagaRoster.CopySlots(board); id = board.Count > 0 && board[0] != null ? board[0].UnitId : 0; } string tip = LifeSagaPresentation.BuildPlainText(id) ?? ""; string any = (cmd.value ?? "").Trim(); pass = false; if (!string.IsNullOrEmpty(tip) && !string.IsNullOrEmpty(any)) { string[] parts = any.Split('|'); for (int i = 0; i < parts.Length; i++) { string needle = (parts[i] ?? "").Trim(); if (needle.Length > 0 && tip.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0) { pass = true; break; } } } detail = $"stakeTip='{tip}' any='{any}' pass={pass}"; break; } case "saga_cast_contains": { long id = EventFeedUtil.SafeId(MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null); if (id == 0) { var board = new System.Collections.Generic.List(LifeSagaRoster.Cap); LifeSagaRoster.CopySlots(board); id = board.Count > 0 && board[0] != null ? board[0].UnitId : 0; } LifeSagaViewModel model = LifeSagaPresentation.Build(id); string any = (cmd.value ?? "").Trim(); pass = false; string sample = ""; if (model != null) { for (int i = 0; i < model.Cast.Count; i++) { LifeSagaCastMember m = model.Cast[i]; if (m == null) { continue; } string row = (m.Relation ?? "") + " " + (m.Name ?? ""); if (i > 0) { sample += " · "; } sample += row; if (!string.IsNullOrEmpty(any) && row.IndexOf(any, StringComparison.OrdinalIgnoreCase) >= 0) { pass = true; } } if (!pass && !string.IsNullOrEmpty(any)) { string[] parts = any.Split('|'); for (int i = 0; i < parts.Length && !pass; i++) { string needle = (parts[i] ?? "").Trim(); if (needle.Length == 0) { continue; } for (int j = 0; j < model.Cast.Count; j++) { LifeSagaCastMember m = model.Cast[j]; if (m == null) { continue; } string row = (m.Relation ?? "") + " " + (m.Name ?? ""); if (row.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0) { pass = true; break; } } } } } detail = $"cast='{sample}' any='{any}' pass={pass}"; break; } case "saga_legacy_contains": { long id = EventFeedUtil.SafeId(MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null); if (id == 0) { var board = new System.Collections.Generic.List(LifeSagaRoster.Cap); LifeSagaRoster.CopySlots(board); id = board.Count > 0 && board[0] != null ? board[0].UnitId : 0; } LifeSagaViewModel model = LifeSagaPresentation.Build(id); string any = (cmd.value ?? "").Trim(); pass = false; string sample = ""; if (model != null) { for (int i = 0; i < model.Legacy.Count; i++) { LifeSagaLegacyBeat b = model.Legacy[i]; if (b == null || string.IsNullOrEmpty(b.Line)) { continue; } if (sample.Length > 0) { sample += " | "; } sample += b.Line; if (!string.IsNullOrEmpty(any) && b.Line.IndexOf(any, StringComparison.OrdinalIgnoreCase) >= 0) { pass = true; } } if (!pass && !string.IsNullOrEmpty(any)) { string[] parts = any.Split('|'); for (int i = 0; i < parts.Length && !pass; i++) { string needle = (parts[i] ?? "").Trim(); if (needle.Length == 0) { continue; } for (int j = 0; j < model.Legacy.Count; j++) { LifeSagaLegacyBeat b = model.Legacy[j]; if (b == null || string.IsNullOrEmpty(b.Line)) { continue; } if (b.Line.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0) { pass = true; break; } } } } } detail = $"legacy='{sample}' any='{any}' pass={pass}"; break; } case "saga_legacy_not": { long id = EventFeedUtil.SafeId(MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null); if (id == 0) { var board = new System.Collections.Generic.List(LifeSagaRoster.Cap); LifeSagaRoster.CopySlots(board); id = board.Count > 0 && board[0] != null ? board[0].UnitId : 0; } LifeSagaViewModel model = LifeSagaPresentation.Build(id); // Preserve trailing spaces on needles (e.g. "Child " must not match "children"). string banned = cmd.value ?? ""; pass = true; string sample = ""; string hit = ""; if (model != null && !string.IsNullOrWhiteSpace(banned)) { string[] parts = banned.Split('|'); for (int i = 0; i < model.Legacy.Count; i++) { LifeSagaLegacyBeat b = model.Legacy[i]; if (b == null || string.IsNullOrEmpty(b.Line)) { continue; } if (sample.Length > 0) { sample += " | "; } sample += b.Line; string line = b.Line.Trim(); int sep = line.IndexOf(" · ", StringComparison.Ordinal); string beat = sep >= 0 ? line.Substring(sep + 3).Trim() : line; for (int p = 0; p < parts.Length; p++) { string needle = parts[p] ?? ""; if (string.IsNullOrWhiteSpace(needle)) { continue; } // Trim only leading whitespace; trailing spaces are intentional prefixes. needle = needle.TrimStart(); bool prefix = needle.Length > 0 && needle[needle.Length - 1] == ' '; bool matched = prefix ? beat.StartsWith(needle, StringComparison.OrdinalIgnoreCase) : beat.IndexOf(needle.Trim(), StringComparison.OrdinalIgnoreCase) >= 0; if (matched) { pass = false; hit = needle; break; } } } } detail = $"legacy='{sample}' banned='{banned}' hit='{hit}' pass={pass}"; break; } case "saga_legacy_quality": { long id = EventFeedUtil.SafeId(MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null); if (id == 0) { var board = new System.Collections.Generic.List(LifeSagaRoster.Cap); LifeSagaRoster.CopySlots(board); id = board.Count > 0 && board[0] != null ? board[0].UnitId : 0; } LifeSagaViewModel model = LifeSagaPresentation.Build(id); var castIds = new HashSet(); int parentChildLines = 0; int namedChildLines = 0; bool hasRawChildLabel = false; bool hasFamilySummary = false; bool hasTurningPoint = false; string sample = ""; if (model != null) { for (int i = 0; i < model.Cast.Count; i++) { if (model.Cast[i] != null && model.Cast[i].UnitId != 0) { castIds.Add(model.Cast[i].UnitId); } } for (int i = 0; i < model.Legacy.Count; i++) { LifeSagaLegacyBeat b = model.Legacy[i]; if (b == null || string.IsNullOrEmpty(b.Line)) { continue; } if (sample.Length > 0) { sample += " | "; } sample += b.Line; string line = b.Line.Trim(); // Strip optional date prefix "Month Year · ". int sep = line.IndexOf(" · ", StringComparison.Ordinal); string beat = sep >= 0 ? line.Substring(sep + 3).Trim() : line; if (beat.StartsWith("Child ", StringComparison.OrdinalIgnoreCase) && !beat.StartsWith("Children", StringComparison.OrdinalIgnoreCase)) { hasRawChildLabel = true; } if (b.Kind == LifeSagaFactKind.ParentChild || beat.StartsWith("Had child", StringComparison.OrdinalIgnoreCase) || beat.StartsWith("Had a child", StringComparison.OrdinalIgnoreCase) || beat.IndexOf(" children", StringComparison.OrdinalIgnoreCase) >= 0) { parentChildLines++; if (beat.StartsWith("Had child ", StringComparison.OrdinalIgnoreCase)) { namedChildLines++; } if (beat.IndexOf(" children", StringComparison.OrdinalIgnoreCase) >= 0 || string.Equals(beat, "Had a child", StringComparison.OrdinalIgnoreCase)) { hasFamilySummary = true; } } if (b.Kind == LifeSagaFactKind.WarJoin || b.Kind == LifeSagaFactKind.WarEnd || b.Kind == LifeSagaFactKind.Kill || b.Kind == LifeSagaFactKind.RivalEarned || b.Kind == LifeSagaFactKind.BondFormed || b.Kind == LifeSagaFactKind.Founding || b.Kind == LifeSagaFactKind.RoleChange || b.Kind == LifeSagaFactKind.PlotJoin || b.Kind == LifeSagaFactKind.HardArcCombat || b.Kind == LifeSagaFactKind.HardArcWar || beat.StartsWith("War", StringComparison.OrdinalIgnoreCase) || beat.StartsWith("Bound", StringComparison.OrdinalIgnoreCase) || beat.StartsWith("Slew", StringComparison.OrdinalIgnoreCase) || beat.StartsWith("Founded", StringComparison.OrdinalIgnoreCase) || beat.StartsWith("Became", StringComparison.OrdinalIgnoreCase)) { hasTurningPoint = true; } } } int parented = LifeSagaMemory.CountParentedChildren(id); bool parentCapOk = parentChildLines <= 1; bool proseOk = !hasRawChildLabel; bool summaryOk = parented < 3 || hasFamilySummary || namedChildLines == 0; if (parented >= 3) { summaryOk = hasFamilySummary && namedChildLines == 0; } // When memory has a non-family turning point, Legacy must surface at least one. bool memoryHasTurning = false; var memFacts = LifeSagaMemory.FactsFor(id); for (int i = 0; i < memFacts.Count; i++) { LifeSagaFact f = memFacts[i]; if (f == null) { continue; } if (f.Kind == LifeSagaFactKind.WarJoin || f.Kind == LifeSagaFactKind.Kill || f.Kind == LifeSagaFactKind.BondFormed || f.Kind == LifeSagaFactKind.Founding || f.Kind == LifeSagaFactKind.RivalEarned || f.Kind == LifeSagaFactKind.PlotJoin) { memoryHasTurning = true; break; } } bool turningOk = !memoryHasTurning || hasTurningPoint; pass = parentCapOk && proseOk && summaryOk && turningOk; detail = $"legacy='{sample}' parented={parented} parentLines={parentChildLines} named={namedChildLines} " + $"rawChild={hasRawChildLabel} summary={hasFamilySummary} turning={hasTurningPoint} " + $"memTurning={memoryHasTurning} cast={castIds.Count} pass={pass}"; break; } case "saga_memory_life_count": { int want = ParseInt(cmd.value, 1); int have = LifeSagaMemory.LifeCount; string cmp = (cmd.label ?? "").Trim().ToLowerInvariant(); pass = cmp == "min" ? have >= want : have == want; detail = $"lives={have} want={want} cmp={cmp}"; break; } case "saga_admission_persisted": { var board = new System.Collections.Generic.List(LifeSagaRoster.Cap); LifeSagaRoster.CopySlots(board); LifeSagaSlot slot = board.Count > 0 ? board[0] : null; pass = slot != null && slot.AdmissionReason != LifeSagaAdmissionReason.None; detail = $"admission={slot?.AdmissionReason.ToString() ?? "missing"} id={slot?.UnitId ?? 0}"; break; } case "saga_focus_admission": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; LifeSagaSlot slot = LifeSagaRoster.Get(EventFeedUtil.SafeId(focus)); string any = (cmd.value ?? "").Trim(); string have = slot?.AdmissionReason.ToString() ?? "missing"; pass = slot != null; if (pass && !string.IsNullOrEmpty(any)) { pass = false; string[] parts = any.Split('|'); for (int i = 0; i < parts.Length; i++) { if (string.Equals( have, (parts[i] ?? "").Trim(), StringComparison.OrdinalIgnoreCase)) { pass = true; break; } } } detail = $"focus={SafeName(focus)} admission={have} pop={slot?.SpeciesPopulation ?? 0} rare={slot?.IsRareCreature ?? false} special={slot?.IsSpecialCreature ?? false}"; break; } case "saga_focus_admission_eligible": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; bool want = ParseBool(cmd.value, defaultValue: true); bool have = LifeSagaRoster.HarnessIsAdmissionCandidate(focus); pass = have == want; detail = $"focus={SafeName(focus)} asset={focus?.asset?.id} eligible={have} want={want}"; break; } case "stability_budgets": { pass = InterestVariety.HarnessProbeStabilityBudgets(out detail); break; } case "beat_significance_policy": { pass = BeatSignificance.HarnessProbePolicy(out detail); break; } case "beat_significance_live_partner": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; pass = BeatSignificance.HarnessProbeLiveRelation( focus, _happinessPartner, out detail); break; } case "saga_replacement_evidence": { pass = LifeSagaRoster.HarnessProbeReplacementEvidence(out detail); break; } case "saga_role_tip": { string have = SampleRailTip(); string any = (cmd.value ?? "").Trim(); pass = false; if (!string.IsNullOrEmpty(have) && !string.IsNullOrEmpty(any)) { string[] parts = any.Split('|'); for (int i = 0; i < parts.Length; i++) { string needle = (parts[i] ?? "").Trim(); if (needle.Length > 0 && have.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0) { pass = true; break; } } } detail = $"roleTip='{have}' any='{any}' pass={pass}"; break; } case "saga_prefer_count": { LifeSagaRoster.Tick(Time.unscaledTime); int want = ParseInt(cmd.value, 1); var board = new System.Collections.Generic.List(LifeSagaRoster.Cap); LifeSagaRoster.CopySlots(board); int have = 0; for (int i = 0; i < board.Count; i++) { if (board[i] != null && board[i].Prefer) { have++; } } string cmp = (cmd.label ?? "").Trim().ToLowerInvariant(); pass = cmp == "min" ? have >= want : have == want; detail = $"preferCount={have} want={want} cmp={cmp} roster={board.Count}"; break; } case "tip_matches_unit": { string tipAsset = CameraDirector.LastWatchAssetId ?? ""; Actor unit = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; string unitAsset = unit?.asset != null ? unit.asset.id : ""; string tip = CameraDirector.LastWatchLabel ?? ""; bool assetsMatch = !string.IsNullOrEmpty(tipAsset) && tipAsset == unitAsset && MoveCamera.hasFocusUnit(); // If the tip claims a species, the focused unit must be that species. bool labelOk = true; const string prefix = "New species: "; if (tip.StartsWith(prefix)) { string claimed = tip.Substring(prefix.Length).Trim(); labelOk = !string.IsNullOrEmpty(claimed) && claimed == unitAsset; } pass = assetsMatch && labelOk; detail = $"tipAsset={tipAsset} unitAsset={unitAsset} tip={tip}"; break; } case "focus_fighting": { Actor unit = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; bool fighting = LiveEnsemble.IsCombatParticipant(unit); bool want = string.IsNullOrEmpty(cmd.value) || !string.Equals(cmd.value, "false", StringComparison.OrdinalIgnoreCase); pass = fighting == want; detail = $"focus={SafeName(unit)} fighting={fighting} want={want}"; break; } case "related_fighting": { Actor unit = InterestDirector.CurrentCandidate?.RelatedUnit; bool fighting = LiveEnsemble.IsCombatParticipant(unit); bool want = string.IsNullOrEmpty(cmd.value) || !string.Equals(cmd.value, "false", StringComparison.OrdinalIgnoreCase); pass = fighting == want; detail = $"related={SafeName(unit)} fighting={fighting} want={want}"; break; } case "tip_matches_focus": { Actor unit = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; string tip = CameraDirector.LastWatchLabel ?? ""; string name = SafeName(unit); if (string.IsNullOrEmpty(tip)) { // Cleared fight tip during combat cold is OK. pass = true; detail = "tip_empty focus=" + name; } else if (string.IsNullOrEmpty(name)) { pass = false; detail = "no_focus tip='" + tip + "'"; } else { long focusId = EventFeedUtil.SafeId(unit); InterestCandidate current = InterestDirector.CurrentCandidate; bool candidateOwnsFocus = current != null && (current.SubjectId == focusId || EventFeedUtil.SafeId(current.FollowUnit) == focusId); bool discoveryOwnsFocus = tip.StartsWith( "New species: ", StringComparison.OrdinalIgnoreCase) && string.Equals( CameraDirector.LastWatchAssetId, unit?.asset?.id, StringComparison.Ordinal); pass = candidateOwnsFocus || discoveryOwnsFocus || tip.StartsWith(name, StringComparison.OrdinalIgnoreCase) || IsEnsembleCombatTip(tip); // Candidate identity is authoritative. The rendered reason may deliberately // omit the subject name, especially for discoveries and semantic Beats. detail = $"tip='{tip}' focus='{name}' candidateOwns={candidateOwnsFocus}" + $" discoveryOwns={discoveryOwnsFocus} pass={pass}"; } break; } case "tip_not_contains": { string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value; string tip = CameraDirector.LastWatchLabel ?? ""; pass = string.IsNullOrEmpty(needle) || tip.IndexOf(needle, StringComparison.OrdinalIgnoreCase) < 0; detail = $"tip='{tip}' not_contains='{needle}'"; break; } case "tip_matches_any": { // value = pipe-separated needles; tip must contain at least one. string raw = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value; string tip = CameraDirector.LastWatchLabel ?? ""; pass = false; if (!string.IsNullOrEmpty(raw)) { string[] parts = raw.Split('|'); for (int i = 0; i < parts.Length; i++) { string needle = (parts[i] ?? "").Trim(); if (needle.Length > 0 && tip.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0) { pass = true; break; } } } detail = $"tip='{tip}' any='{raw}' pass={pass}"; break; } case "participant_count_min": { int want = 1; if (!int.TryParse(string.IsNullOrEmpty(cmd.value) ? "1" : cmd.value, out want)) { want = 1; } int have = InterestDirector.CurrentCandidate?.ParticipantCount ?? 0; pass = have >= want; detail = $"participants={have} want>={want}"; break; } case "unit_asset": { string wantAsset = string.IsNullOrEmpty(cmd.asset) ? cmd.value : cmd.asset; if (wantAsset == "auto" || string.IsNullOrEmpty(wantAsset)) { wantAsset = _lastSpawnedAssetId; } Actor unit = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; string unitAsset = unit?.asset != null ? unit.asset.id : ""; if (!string.IsNullOrEmpty(wantAsset) && wantAsset.IndexOf('|') >= 0) { string[] opts = wantAsset.Split('|'); pass = false; for (int i = 0; i < opts.Length; i++) { if (opts[i].Trim().Equals(unitAsset, StringComparison.OrdinalIgnoreCase)) { pass = true; break; } } } else { pass = !string.IsNullOrEmpty(wantAsset) && wantAsset == unitAsset; } detail = $"unitAsset={unitAsset} want={wantAsset}"; break; } case "no_bad": { pass = StateProbe.BadEventCount == 0; detail = $"bad={StateProbe.BadEventCount}"; break; } case "drop_contains": { string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value; pass = !string.IsNullOrEmpty(needle) && InterestDropLog.RecentContains(needle); detail = $"needle='{needle}' drops='{InterestDropLog.FormatRecent(8)}'"; break; } case "no_placeholder_tip": { string formatted = CameraDirector.LastFormattedWatchTip ?? ""; pass = formatted.IndexOf("something interesting", StringComparison.OrdinalIgnoreCase) < 0; detail = $"formattedTip='{formatted}'"; break; } case "has_focus": { bool want = ParseBool(cmd.value, defaultValue: true); bool has = MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null && MoveCamera._focus_unit.isAlive(); pass = has == want; detail = $"hasFocus={has} want={want}"; break; } case "tip_contains": { string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value; if (needle == "auto" || needle.Contains("{asset}")) { needle = (needle == "auto" ? "New species: {asset}" : needle) .Replace("{asset}", _lastSpawnedAssetId ?? ""); } string tip = CameraDirector.LastWatchLabel ?? ""; pass = !string.IsNullOrEmpty(needle) && tip.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0; detail = $"tip='{tip}' contains='{needle}'"; break; } case "tip_contains_or_story_hold": { string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value; string tip = CameraDirector.LastWatchLabel ?? ""; bool contains = !string.IsNullOrEmpty(needle) && tip.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0; bool held = StoryScheduler.ActiveEpisode != null && !contains; pass = contains || held; detail = $"tip='{tip}' contains='{needle}' storyHold={held}" + $" episode={StoryScheduler.ActiveEpisode?.ThreadId ?? "none"}"; break; } case "enabled": case "setting_enabled": { bool want = ParseBool(cmd.value, defaultValue: true); pass = ModSettings.Enabled == want; detail = $"enabled={ModSettings.Enabled} want={want}"; break; } case "show_watch_reasons": { bool want = ParseBool(cmd.value, defaultValue: true); pass = ModSettings.ShowWatchReasons == want; detail = $"show_watch_reasons={ModSettings.ShowWatchReasons} want={want}"; break; } case "current_tier": { // Legacy name: prefer tip/label match; else score band from old tier names. string want = cmd.value ?? ""; string label = InterestDirector.CurrentLabel ?? ""; string tip = CameraDirector.LastWatchLabel ?? ""; float score = InterestDirector.CurrentScore; bool byText = (!string.IsNullOrEmpty(want) && (label.IndexOf(want, StringComparison.OrdinalIgnoreCase) >= 0 || tip.IndexOf(want, StringComparison.OrdinalIgnoreCase) >= 0)); bool byBand = false; string band = want.Trim().ToLowerInvariant(); if (band == "ambient") { byBand = InterestScoring.IsFillScore(score); } else if (band == "curiosity") { byBand = score >= 35f && score < InterestScoringConfig.W.noticeScoreMin; } else if (band == "action") { byBand = score >= InterestScoringConfig.W.noticeScoreMin; } else if (band == "story") { byBand = score >= 75f; } else if (band == "epic") { byBand = InterestScoring.IsHotScore(score) || score >= 95f; } else if (band == "none") { byBand = InterestDirector.CurrentCandidate == null; } pass = byText || byBand; detail = $"want='{want}' label='{label}' tip='{tip}' score={score:0.#} text={byText} band={byBand}"; break; } case "would_accept_curiosity": { bool want = ParseBool(cmd.value, defaultValue: true); bool have = InterestDirector.WouldAcceptCuriosity(); pass = have == want; detail = $"would_accept={have} want={want} score={InterestDirector.CurrentScoreLabel}"; break; } case "focus_same": case "focus_unchanged": { string nowKey = FocusKey(); pass = !string.IsNullOrEmpty(_rememberedFocusKey) && _rememberedFocusKey != "-" && nowKey == _rememberedFocusKey; detail = $"remembered={_rememberedFocusKey} now={nowKey}"; break; } case "tip_same": case "tip_unchanged": { string nowTip = InterestDirector.CurrentCandidate?.Label ?? CameraDirector.LastWatchLabel ?? ""; pass = !string.IsNullOrEmpty(_rememberedTip) && nowTip == _rememberedTip; detail = $"remembered='{_rememberedTip}' now='{nowTip}'"; break; } case "related_same": case "pair_partner_same": { InterestCandidate scene = InterestDirector.CurrentCandidate; long nowId = 0; if (scene != null) { nowId = scene.PairPartnerId != 0 ? scene.PairPartnerId : EventFeedUtil.SafeId(scene.RelatedUnit); } pass = _rememberedRelatedId != 0 && nowId == _rememberedRelatedId; detail = $"remembered={_rememberedRelatedId} now={nowId}"; break; } case "focus_arrows": case "relationship_arrows": { bool pinned = FocusRelationshipArrows.IsPinnedToFocus(); pass = SpectatorMode.Active && MoveCamera.hasFocusUnit() && pinned; detail = $"idle={SpectatorMode.Active} focus={MoveCamera.hasFocusUnit()} pinned={pinned} lastActor={(UnitSelectionEffect.last_actor != null ? SafeName(UnitSelectionEffect.last_actor) : "null")} dest={PlayerConfig.optionBoolEnabled("cursor_arrow_destination")} lover={PlayerConfig.optionBoolEnabled("cursor_arrow_lover")} attack={PlayerConfig.optionBoolEnabled("cursor_arrow_attack_target")}"; break; } case "presented": { string assetId = ResolveAssetId(cmd); bool want = string.IsNullOrEmpty(cmd.value) || ParseBool(cmd.value, true); bool have = SpeciesDiscovery.WasPresented(assetId); pass = have == want; detail = $"asset={assetId} presented={have} want={want}"; break; } case "pending_discovery": case "discovery_pending": { int want = ParseCountExpect(cmd, defaultValue: 0); int have = SpeciesDiscovery.PendingCount; pass = have == want; detail = $"pending={have} want={want}"; break; } case "pending_discovery_asset": { string assetId = ResolveAssetId(cmd); bool want = string.IsNullOrEmpty(cmd.value) || ParseBool(cmd.value, true); bool have = SpeciesDiscovery.IsPending(assetId); pass = have == want; detail = $"asset={assetId} pending={have} want={want} total={SpeciesDiscovery.PendingCount}"; break; } case "pending_interest": { int want = ParseCountExpect(cmd, defaultValue: 0); int have = InterestCollector.PendingCount; pass = have == want; detail = $"interest_pending={have} want={want}"; break; } case "pending_contains": { string needle = (cmd.value ?? cmd.label ?? "").Trim(); int have = InterestRegistry.CountKeysContaining(needle); pass = !string.IsNullOrEmpty(needle) && have > 0; detail = $"needle='{needle}' matches={have} pending={InterestRegistry.PendingCount}"; break; } case "scoring_config_loaded": { bool want = string.IsNullOrEmpty(cmd.value) || ParseBool(cmd.value, true); bool have = InterestScoringConfig.LoadedFromFile; pass = have == want; detail = $"loaded={have} want={want} v={InterestScoringConfig.LoadedVersion} charEvent={InterestScoringConfig.W.charWeightEventLed:0.##}"; break; } case "session_key": case "interest_session_key": { string want = (cmd.value ?? cmd.expect ?? "").Trim(); string have = InterestDirector.CurrentKey ?? ""; pass = !string.IsNullOrEmpty(want) && have.IndexOf(want, System.StringComparison.OrdinalIgnoreCase) >= 0; detail = $"key='{have}' contains='{want}'"; break; } case "session_active": case "interest_session_active": { bool want = ParseBool(cmd.value, defaultValue: true); bool have = InterestDirector.CurrentIsActive; pass = have == want; detail = $"active={have} want={want} key={InterestDirector.CurrentKey}"; break; } case "event_share": case "interest_event_share": { float want = ParseFloat(cmd.value, 0.5f); float have = InterestVariety.EventShare(); float tol = 0.35f; if (!string.IsNullOrEmpty(cmd.label) && float.TryParse(cmd.label, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsedTol)) { tol = parsedTol; } pass = InterestVariety.MixSampleCount < 3 || Mathf.Abs(have - want) <= tol; detail = $"share={have:0.##} want~={want:0.##} tol={tol:0.##} samples={InterestVariety.MixSampleCount}"; break; } case "happiness_drain_seq": { ulong have = HappinessEventRouter.CurrentSequence; pass = have >= 0; detail = $"happiness_seq={have}"; break; } case "happiness_cursor": { ulong have = InterestFeeds.HappinessCursor; if (!string.IsNullOrEmpty(cmd.value) && ulong.TryParse(cmd.value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong want)) { pass = have == want; detail = $"cursor={have} want={want}"; } else { pass = true; detail = $"cursor={have}"; } break; } case "interest_has_key": { string want = (cmd.value ?? cmd.expect ?? "").Trim(); bool have = false; string found = ""; if (!string.IsNullOrEmpty(want)) { if ((InterestDirector.CurrentKey ?? "").IndexOf(want, System.StringComparison.OrdinalIgnoreCase) >= 0) { have = true; found = InterestDirector.CurrentKey; } else { var pending = new System.Collections.Generic.List(64); InterestRegistry.CopyPending(pending); for (int i = 0; i < pending.Count; i++) { InterestCandidate c = pending[i]; if (c?.Key != null && c.Key.IndexOf(want, System.StringComparison.OrdinalIgnoreCase) >= 0) { have = true; found = c.Key; break; } if (c?.HappinessEffectId != null && c.HappinessEffectId.IndexOf(want, System.StringComparison.OrdinalIgnoreCase) >= 0) { have = true; found = c.Key; break; } } if (!have && InterestRegistry.CountKeysContaining(want) > 0) { have = true; found = "registry:" + want; } } } pass = have; detail = $"want='{want}' found='{found}' pending={InterestRegistry.PendingCount}"; break; } case "interest_label_contains": { string want = (cmd.value ?? cmd.expect ?? "").Trim(); bool have = false; string found = ""; if (!string.IsNullOrEmpty(want)) { string currentLabel = InterestDirector.CurrentLabel ?? ""; if (currentLabel.IndexOf(want, StringComparison.OrdinalIgnoreCase) >= 0) { have = true; found = currentLabel; } else { var pending = new System.Collections.Generic.List(64); InterestRegistry.CopyPending(pending); for (int i = 0; i < pending.Count; i++) { InterestCandidate c = pending[i]; string label = c?.Label ?? ""; if (label.IndexOf(want, StringComparison.OrdinalIgnoreCase) >= 0) { have = true; found = label; break; } } } } pass = have; detail = $"want='{want}' found='{found}' pending={InterestRegistry.PendingCount}"; break; } case "interest_no_key": { string want = (cmd.value ?? cmd.expect ?? "").Trim(); int n = InterestRegistry.CountKeysContaining(want); bool inCurrent = !string.IsNullOrEmpty(want) && (InterestDirector.CurrentKey ?? "") .IndexOf(want, StringComparison.OrdinalIgnoreCase) >= 0; pass = n == 0 && !inCurrent; detail = $"want_absent='{want}' count={n} current={InterestDirector.CurrentKey}"; break; } case "interest_registry_count": { string needle = cmd.label ?? ""; int want = ParseCountExpect(cmd, defaultValue: 0); int have = string.IsNullOrEmpty(needle) ? InterestRegistry.Count : InterestRegistry.CountKeysContaining(needle); pass = have == want; detail = $"count={have} want={want} needle='{needle}'"; break; } case "interrupted_key": case "interest_interrupted": { string want = (cmd.value ?? "").Trim(); string have = InterestDirector.InterruptedKey ?? ""; if (string.IsNullOrEmpty(want)) { pass = !string.IsNullOrEmpty(have); } else if (want.Equals("empty", StringComparison.OrdinalIgnoreCase) || want.Equals("none", StringComparison.OrdinalIgnoreCase)) { pass = string.IsNullOrEmpty(have); } else { pass = have.IndexOf(want, StringComparison.OrdinalIgnoreCase) >= 0; } detail = $"interrupted='{have}' want='{want}'"; break; } case "interest_lead": { string want = (cmd.value ?? "EventLed").Trim(); InterestLeadKind? have = InterestDirector.CurrentLeadKind; pass = have.HasValue && have.Value.ToString().Equals(want, StringComparison.OrdinalIgnoreCase); detail = $"lead={have} want={want} key={InterestDirector.CurrentKey}"; break; } case "interest_peek_lead": { string want = (cmd.value ?? "EventLed").Trim(); InterestCandidate peek = InterestDirector.HarnessPeekNext(); pass = peek != null && peek.LeadKind.ToString().Equals(want, StringComparison.OrdinalIgnoreCase); detail = peek == null ? "peek=null" : $"peek_lead={peek.LeadKind} want={want} key={peek.Key}"; break; } case "interest_score_order": { // value = winnerNeedle, label = loserNeedle — winner TotalScore must be >= loser. string winnerNeedle = (cmd.value ?? "").Trim(); string loserNeedle = (cmd.label ?? "").Trim(); InterestCandidate win = null; InterestCandidate lose = null; var all = new System.Collections.Generic.List(64); InterestRegistry.CopyPending(all); if (InterestDirector.CurrentCandidate != null) { all.Add(InterestDirector.CurrentCandidate); } for (int i = 0; i < all.Count; i++) { InterestCandidate c = all[i]; if (c?.Key == null) { continue; } if (win == null && c.Key.IndexOf(winnerNeedle, StringComparison.OrdinalIgnoreCase) >= 0) { win = c; } if (lose == null && c.Key.IndexOf(loserNeedle, StringComparison.OrdinalIgnoreCase) >= 0) { lose = c; } } if (win != null) { InterestScoring.RecalcTotal(win); } if (lose != null) { InterestScoring.RecalcTotal(lose); } pass = win != null && lose != null && win.TotalScore >= lose.TotalScore; detail = $"win={win?.Key}({win?.TotalScore:0.#}) lose={lose?.Key}({lose?.TotalScore:0.#})" + $" arc={InterestVariety.LastArcKey} streak={InterestVariety.ArcStreak}"; break; } case "variety_arc": { string want = (cmd.value ?? "").Trim(); string have = InterestVariety.LastArcKey ?? ""; pass = !string.IsNullOrEmpty(want) && have.IndexOf(want, StringComparison.OrdinalIgnoreCase) >= 0; detail = $"arc='{have}' want='{want}' streak={InterestVariety.ArcStreak}"; break; } case "variety_arc_streak": { int want = ParseInt(cmd.value, 1); pass = InterestVariety.ArcStreak == want; detail = $"streak={InterestVariety.ArcStreak} want={want} arc={InterestVariety.LastArcKey}"; break; } case "civic_boost": { string effectId = (cmd.value ?? "").Trim(); bool want = string.IsNullOrEmpty(cmd.label) || ParseBool(cmd.label, true); bool have = InterestFeeds.HasCivicBoostContaining(effectId); pass = have == want; detail = $"civic={have} want={want} effect='{effectId}' count={InterestFeeds.CivicBoostCount}"; break; } case "in_grace": { bool want = ParseBool(cmd.value, defaultValue: true); bool have = InterestDirector.InInputGrace; pass = have == want; detail = $"in_grace={have} want={want}"; break; } case "dossier_contains": case "caption_contains": { string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value; if (needle == "auto" || string.IsNullOrEmpty(needle)) { needle = _lastSpawnedAssetId; } string caption = WatchCaption.LastCaptionText ?? ""; UnitDossier dossier = WatchCaption.Current; bool dossierHit = dossier != null && dossier.ContainsIgnoreCase(needle); bool captionHit = !string.IsNullOrEmpty(needle) && caption.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0; pass = dossierHit || captionHit; detail = $"needle='{needle}' caption='{caption.Replace("\n", " | ")}' dossier={(dossier != null)} reason='{dossier?.ReasonLine}'"; break; } case "caption_not_contains": { string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value; string caption = WatchCaption.LastCaptionText ?? ""; string detailLine = WatchCaption.LastDetail ?? ""; UnitDossier dossier = WatchCaption.Current; string dossierDetail = dossier?.DetailLine ?? ""; bool hit = (!string.IsNullOrEmpty(needle) && (caption.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0 || detailLine.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0 || dossierDetail.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0)); pass = !hit; detail = $"needle='{needle}' hit={hit} caption='{caption.Replace("\n", " | ")}' detail='{detailLine}'"; break; } case "tooltip_library_ok": { pass = DossierAssetTips.RunTooltipInventoryAudit(HarnessDir(), out string tipDetail); detail = tipDetail; break; } case "history_other_rows": { WatchCaption.ForceRefreshHistory(); int have = WatchCaption.CountHistoryOtherRows(); int want = 1; if (!string.IsNullOrEmpty(cmd.value) && int.TryParse(cmd.value.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed)) { want = parsed; } string cmp = (cmd.label ?? "min").Trim().ToLowerInvariant(); pass = cmp == "exact" ? have == want : have >= want; long subjectId = WatchCaption.CurrentUnitId; if (subjectId == 0) { subjectId = ResolveActivitySubjectId(); } int withRelated = 0; var bits = new System.Text.StringBuilder(); IReadOnlyList ring = ActivityLog.LatestForSubject(subjectId, 8); for (int i = 0; i < ring.Count; i++) { ActivityEntry e = ring[i]; if (e == null) { continue; } if (e.RelatedId != 0 && e.RelatedId != subjectId) { withRelated++; } if (bits.Length > 0) { bits.Append(';'); } bits.Append(e.TaskId ?? "") .Append(':') .Append(e.RelatedId) .Append(':') .Append(e.TargetLabel ?? ""); } detail = $"otherRows={have} want={want} cmp={cmp} subject={subjectId} visible={WatchCaption.Visible} " + $"ringRelated={withRelated} ring=[{bits}] hist='{WatchCaption.LastHistoryJoined}'"; break; } case "lore_detail_is_other": { long loreId = ChronicleHud.DetailUnitId; long dossierId = WatchCaption.CurrentUnitId; long cameraId = 0; bool cameraFollow = false; try { if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null) { cameraId = MoveCamera._focus_unit.getID(); cameraFollow = MoveCamera.isCameraFollowingUnit(MoveCamera._focus_unit); } } catch { cameraId = 0; cameraFollow = false; } bool fallenKiller = (Chronicle.LastFallenFocusDetail ?? "") .StartsWith("killer:", System.StringComparison.Ordinal); // After row-click OpenUnitHistory(other): Lore + dossier + camera on that living unit. bool dossierOk = loreId != 0 && loreId == dossierId; bool cameraOk = fallenKiller || (cameraId != 0 && cameraId == loreId && cameraFollow); pass = dossierOk && cameraOk; detail = $"loreId={loreId} dossierId={dossierId} cameraId={cameraId} cameraFollow={cameraFollow} " + $"follow={ChronicleHud.FollowFocus} fallenKiller={fallenKiller}"; break; } case "reason_names_colored": { string reason = WatchCaption.Current?.ReasonLine ?? ""; bool hasColor = reason.IndexOf(ActivityProse.NameColorHex, System.StringComparison.OrdinalIgnoreCase) >= 0 || reason.IndexOf("= 0; pass = hasColor && !string.IsNullOrEmpty(reason); detail = $"colored={hasColor} reason='{reason}'"; break; } case "reason_other_clickable": { bool want = string.IsNullOrEmpty(cmd.value) || !string.Equals(cmd.value.Trim(), "false", System.StringComparison.OrdinalIgnoreCase); bool have = WatchCaption.ReasonOtherClickable; pass = have == want; detail = $"clickable={have} want={want} relatedId={WatchCaption.Current?.ReasonRelatedId ?? 0} " + $"reason='{WatchCaption.Current?.ReasonLine}'"; break; } case "fallen_killer_banner": { string banner = WatchCaption.LastCaptionText ?? ""; string reason = ""; try { // Banner is shown on reason text while status banner is active. reason = WatchCaption.Current?.ReasonLine ?? ""; } catch { reason = ""; } string hay = (banner + " " + reason).ToLowerInvariant(); bool has = hay.IndexOf("camera on killer", System.StringComparison.Ordinal) >= 0; bool killerFocus = (Chronicle.LastFallenFocusDetail ?? "") .StartsWith("killer:", System.StringComparison.Ordinal); pass = killerFocus && has; detail = $"fallenFocus={Chronicle.LastFallenFocusDetail} bannerHit={has} caption='{banner}'"; break; } case "citizen_job_labels_ok": { // Full live citizen_job_library: non-empty Title Case labels; resource-role // families reorder; bare-prefix siblings (miner_deposit) do not. List ids = ActivityAssetCatalog.EnumerateLiveCitizenJobIds(); int empty = 0; int underscored = 0; int familyBad = 0; int siblingBad = 0; var lines = new List(ids.Count + 1) { "id\tlabel" }; var prefixCounts = new Dictionary(StringComparer.OrdinalIgnoreCase); var idSet = new HashSet(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < ids.Count; i++) { string rawId = ids[i]; if (string.IsNullOrEmpty(rawId)) { continue; } idSet.Add(rawId); int us = rawId.IndexOf('_'); if (us > 0 && us < rawId.Length - 1) { string p = rawId.Substring(0, us); prefixCounts[p] = prefixCounts.TryGetValue(p, out int c) ? c + 1 : 1; } } for (int i = 0; i < ids.Count; i++) { string id = ids[i]; string label = ActivityAssetCatalog.FormatCitizenJobIdLabel(id); lines.Add(id + "\t" + label); if (string.IsNullOrEmpty(label)) { empty++; continue; } if (label.IndexOf('_') >= 0) { underscored++; } int us = id.IndexOf('_'); if (us <= 0 || us >= id.Length - 1) { continue; } string prefix = id.Substring(0, us); bool family = prefixCounts.TryGetValue(prefix, out int pc) && pc >= 2 && !idSet.Contains(prefix); string titlePrefix = ActivityAssetCatalog.TitleCaseWords(prefix); if (family) { // gatherer_herbs → Herb Gatherer (role last) if (!label.EndsWith(titlePrefix, StringComparison.Ordinal) || label.StartsWith(titlePrefix + " ", StringComparison.Ordinal)) { familyBad++; } } else if (idSet.Contains(prefix) && label.EndsWith(titlePrefix, StringComparison.Ordinal) && !label.StartsWith(titlePrefix, StringComparison.Ordinal)) { // miner_deposit must stay Miner Deposit, not Deposit Miner siblingBad++; } } try { System.IO.File.WriteAllLines( System.IO.Path.Combine(HarnessDir(), "citizen-job-labels.tsv"), lines); } catch { // diagnostic dump only } pass = ids.Count > 0 && empty == 0 && underscored == 0 && familyBad == 0 && siblingBad == 0; detail = $"jobs={ids.Count} empty={empty} underscored={underscored} " + $"familyBad={familyBad} siblingBad={siblingBad} sample='{(ids.Count > 0 ? ActivityAssetCatalog.FormatCitizenJobIdLabel(ids[0]) : "")}'"; break; } case "library_asset_labels_ok": { // Every live library id used by EventReason.Library must resolve to a tip // without snake_case, and item tips must not use "forges" except forge-y ids. int total = 0; int empty = 0; int underscored = 0; int forgeBad = 0; var lines = new List { "kind\tid\tdisplay\tsample_label" }; IReadOnlyList kinds = LibraryAssetNames.AllKinds; for (int k = 0; k < kinds.Count; k++) { string kind = kinds[k]; List ids = LibraryAssetNames.EnumerateLiveIds(kind); for (int i = 0; i < ids.Count; i++) { string id = ids[i]; if (string.IsNullOrEmpty(id)) { continue; } total++; string display = LibraryAssetNames.DisplayName(kind, id); string sample = EventReason.Library(null, kind, id); lines.Add(kind + "\t" + id + "\t" + display + "\t" + sample); if (string.IsNullOrEmpty(display)) { empty++; } if (!string.IsNullOrEmpty(display) && display.IndexOf('_') >= 0) { underscored++; } if (kind == "item") { bool forgey = id.IndexOf("forge", StringComparison.OrdinalIgnoreCase) >= 0 || id.IndexOf("smith", StringComparison.OrdinalIgnoreCase) >= 0 || id.IndexOf("anvil", StringComparison.OrdinalIgnoreCase) >= 0; string verb = LibraryAssetNames.ItemVerb(id); if (!forgey && string.Equals(verb, "forges", StringComparison.OrdinalIgnoreCase)) { forgeBad++; } } } } try { System.IO.File.WriteAllLines( System.IO.Path.Combine(HarnessDir(), "library-asset-labels.tsv"), lines); } catch { // diagnostic dump only } pass = total > 0 && empty == 0 && underscored == 0 && forgeBad == 0; detail = $"assets={total} empty={empty} underscored={underscored} forgeBad={forgeBad} " + $"kinds={kinds.Count}"; break; } case "camera_a_inventory_ok": { CameraAInventoryResult inv = CameraAInventoryHarness.RunAudit(HarnessDir()); pass = inv.Passed; detail = inv.Detail; break; } case "library_ambient_policy_ok": { // Signal-only libraries: ambient dials must be B; phenotypes all B; // spectacle spells stay A; utility spells stay B. int dialBad = 0; int phenotypeCamera = 0; int spellFxCamera = 0; int spellSpectacleMissing = 0; int signalCamera = 0; var lines = new List { "domain\tid\tcreates_interest\tstrength\tnotes" }; string[] signalOnlyKeys = { "spell", "power", "item", "gene", "phenotype", "worldLaw", "subspeciesTrait", "biome", "cultureTrait", "religionTrait", "clanTrait", "languageTrait" }; for (int i = 0; i < signalOnlyKeys.Length; i++) { string key = signalOnlyKeys[i]; if (EventCatalogConfig.TryGetLibraryDefaults(key, out LibraryCatalogDefaults dial) && dial != null && dial.AmbientCreatesInterest) { dialBad++; lines.Add(key + "\t*\t\t\tambientCreatesInterest_true"); } } foreach (string id in EventCatalog.Phenotype.AuthoredIds) { DiscreteEventEntry e = EventCatalog.Phenotype.GetOrFallback(id); lines.Add("phenotypes\t" + id + "\t" + e.CreatesInterest + "\t" + e.EventStrength.ToString("0.#") + "\t"); if (e.CreatesInterest) { phenotypeCamera++; } } string[] fxSpells = { "cast_silence", "cast_grass_seeds", "spawn_vegetation", "spawn_skeleton", "cast_shield", "cast_cure" }; for (int i = 0; i < fxSpells.Length; i++) { DiscreteEventEntry e = EventCatalog.Spell.GetOrFallback(fxSpells[i]); lines.Add("spells\t" + fxSpells[i] + "\t" + e.CreatesInterest + "\t" + e.EventStrength.ToString("0.#") + "\tfx"); if (e.CreatesInterest) { spellFxCamera++; } } string[] spectacleSpells = { "summon_lightning", "summon_tornado", "cast_curse", "cast_fire", "cast_blood_rain", "teleport" }; for (int i = 0; i < spectacleSpells.Length; i++) { DiscreteEventEntry e = EventCatalog.Spell.GetOrFallback(spectacleSpells[i]); lines.Add("spells\t" + spectacleSpells[i] + "\t" + e.CreatesInterest + "\t" + e.EventStrength.ToString("0.#") + "\tspectacle"); if (!e.CreatesInterest) { spellSpectacleMissing++; } else { signalCamera++; } } // Mage / warlock creature drop powers must be camera Signal (soak: evil mages ignored). string[] magePowers = { "evil_mage", "white_mage", "necromancer", "plague_doctor", "druid" }; int magePowerMissing = 0; for (int i = 0; i < magePowers.Length; i++) { DiscreteEventEntry e = EventCatalog.Power.GetOrFallback(magePowers[i]); lines.Add("powers\t" + magePowers[i] + "\t" + e.CreatesInterest + "\t" + e.EventStrength.ToString("0.#") + "\tmage"); if (!e.CreatesInterest) { magePowerMissing++; } else { signalCamera++; } } // Genes / clan / language are all B (never own the camera). int geneCamera = 0; foreach (string id in EventCatalog.Gene.AuthoredIds) { if (EventCatalog.Gene.GetOrFallback(id).CreatesInterest) { geneCamera++; } } int clanCamera = 0; foreach (string id in EventCatalog.ClanTrait.AuthoredIds) { if (EventCatalog.ClanTrait.GetOrFallback(id).CreatesInterest) { clanCamera++; } } int languageCamera = 0; foreach (string id in EventCatalog.LanguageTrait.AuthoredIds) { if (EventCatalog.LanguageTrait.GetOrFallback(id).CreatesInterest) { languageCamera++; } } // Spot-check other Signal classes still have camera rows after demote. int lawSignal = 0; foreach (string id in EventCatalog.WorldLaw.AuthoredIds) { if (EventCatalog.WorldLaw.GetOrFallback(id).CreatesInterest) { lawSignal++; } } int itemSignal = 0; foreach (string id in EventCatalog.Item.AuthoredIds) { if (EventCatalog.Item.GetOrFallback(id).CreatesInterest) { itemSignal++; } } try { System.IO.File.WriteAllLines( System.IO.Path.Combine(HarnessDir(), "library-ambient-policy.tsv"), lines); } catch { // diagnostic dump only } int wantSignal = spectacleSpells.Length + magePowers.Length; pass = dialBad == 0 && phenotypeCamera == 0 && geneCamera == 0 && clanCamera == 0 && languageCamera == 0 && spellFxCamera == 0 && spellSpectacleMissing == 0 && magePowerMissing == 0 && lawSignal > 0 && itemSignal > 0 && signalCamera == wantSignal; detail = $"dialBad={dialBad} phenotypeCamera={phenotypeCamera} geneCamera={geneCamera} " + $"clanCamera={clanCamera} languageCamera={languageCamera} " + $"spellFxCamera={spellFxCamera} spellSpectacleMissing={spellSpectacleMissing} " + $"magePowerMissing={magePowerMissing} " + $"lawSignal={lawSignal} itemSignal={itemSignal} spectacleOk={signalCamera}/{wantSignal}"; break; } case "dossier_not_contains": { string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value; UnitDossier dossier = WatchCaption.Current; string reason = dossier?.ReasonLine ?? ""; string caption = WatchCaption.LastCaptionText ?? ""; bool hit = (!string.IsNullOrEmpty(needle) && reason.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0) || (!string.IsNullOrEmpty(needle) && caption.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0) || (dossier != null && dossier.ContainsIgnoreCase(needle)); // Prefer reason-row check: fail only if the reason line (or full dossier) still shows the bad phrase. bool reasonHit = !string.IsNullOrEmpty(needle) && reason.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0; pass = !reasonHit; detail = $"needle_absent_in_reason='{needle}' reason='{reason}' hit={reasonHit}"; break; } case "reason_owns_focus": { // Reason must not name a different live unit than the owned active candidate. // Non-empty reason also requires focus to be a tip principal (duel pair / roster). UnitDossier dossier = WatchCaption.Current; string reason = dossier?.ReasonLine ?? ""; Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; InterestCandidate owned = focus != null ? InterestDirector.TryGetOwnedReasonCandidate(focus) : null; if (string.IsNullOrEmpty(reason)) { pass = true; detail = "empty_reason_ok active=" + InterestDirector.CurrentIsActive; break; } if (owned == null || focus == null) { pass = false; detail = $"reason_without_owned_active focus={(focus != null)} active={InterestDirector.CurrentIsActive} reason='{reason}'"; break; } if (!InterestDirector.UnitIsReasonPrincipal(focus, owned)) { pass = false; detail = $"reason_without_principal reason='{reason}'"; break; } string stranger = FindStrangerNamedInReason(reason, owned); pass = string.IsNullOrEmpty(stranger); detail = pass ? $"owned reason='{reason}'" : $"stranger_in_reason='{stranger}' reason='{reason}'"; break; } case "reason_empty": { UnitDossier dossier = WatchCaption.Current; string reason = dossier?.ReasonLine ?? ""; bool wantEmpty = string.IsNullOrEmpty(cmd.value) || ParseBool(cmd.value, true); bool isEmpty = string.IsNullOrEmpty(reason); pass = isEmpty == wantEmpty; detail = $"reason='{reason}' empty={isEmpty} wantEmpty={wantEmpty} active={InterestDirector.CurrentIsActive} quiet={InterestDirector.InQuietGrace}"; break; } case "reason_matches_any": { // value = pipe-separated needles; orange ReasonLine must contain at least one. string raw = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value; string reason = WatchCaption.Current?.ReasonLine ?? ""; pass = false; if (!string.IsNullOrEmpty(raw)) { string[] parts = raw.Split('|'); for (int i = 0; i < parts.Length; i++) { string needle = (parts[i] ?? "").Trim(); if (needle.Length > 0 && reason.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0) { pass = true; break; } } } detail = $"reason='{reason}' any='{raw}' pass={pass}"; break; } case "outbreak_worthy": { string statusId = (cmd.value ?? "").Trim(); bool want = ParseBool(cmd.label, defaultValue: true); bool have = StatusOutbreakFeed.IsOutbreakWorthy(statusId); pass = have == want; detail = $"status={statusId} worthy={have} want={want}"; break; } case "decision_worthy": { string decisionId = (cmd.value ?? "").Trim(); bool want = ParseBool(cmd.label, defaultValue: true); bool have = EventCatalog.Decision.IsCameraWorthy(decisionId); pass = have == want; detail = $"decision={decisionId} worthy={have} want={want}"; break; } case "dossier_task_refresh": { // Stale focus snapshot must recover: blank dossier TaskText, then sync from live AI. // Empty live AI is ok - chip may keep a sticky prior label (never cleared on gaps). Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; string live = UnitDossier.ReadLiveTask(focus); string after = WatchCaption.HarnessBlankAndRefreshTask(); string chip = WatchCaption.ShownTaskChipText; if (string.IsNullOrEmpty(live)) { pass = string.IsNullOrEmpty(after); detail = $"live_empty dossier='{after}' sticky_chip='{chip}'"; break; } pass = after == live && !string.IsNullOrEmpty(chip); detail = $"live='{live}' dossier='{after}' chip='{chip}'"; break; } case "chronicle_count": { int want = ParseCountExpect(cmd, defaultValue: 1); int have = Chronicle.HistoryCountFor(Chronicle.CurrentHistorySubjectId()); string cmp = (cmd.label ?? "").Trim().ToLowerInvariant(); if (cmp == "min" || cmp == ">=" || string.IsNullOrEmpty(cmp)) { pass = have >= want; detail = $"history_count={have} min={want}"; } else if (cmp == "world") { have = Chronicle.WorldCount; pass = have >= want; detail = $"world_count={have} min={want}"; } else { pass = have == want; detail = $"history_count={have} want={want}"; } break; } case "chronicle_latest_contains": { string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value; if (needle == "auto") { needle = _lastSpawnedAssetId; } ChronicleEntry latest = Chronicle.Latest; string line = latest != null ? latest.Line : ""; string display = latest != null ? latest.DisplayLine : ""; bool inLatest = latest != null && !string.IsNullOrEmpty(needle) && line.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0; // Live lover/friend patches can append after a death sample in the same wait; // still pass when the needle exists on the focus subject's recent history. bool inRecent = false; if (!inLatest && !string.IsNullOrEmpty(needle)) { IReadOnlyList snap = Chronicle.SnapshotForFocus(); for (int i = snap.Count - 1; i >= 0 && i >= snap.Count - 8; i--) { ChronicleEntry e = snap[i]; if (e != null && !string.IsNullOrEmpty(e.Line) && e.Line.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0) { inRecent = true; display = e.DisplayLine; break; } } } pass = inLatest || inRecent; detail = $"latest='{(latest != null ? latest.DisplayLine : "")}' hit='{display}' needle='{needle}' date='{(latest != null ? latest.DateLabel : "")}' recent={inRecent}"; break; } case "chronicle_latest_dated": { ChronicleEntry latest = Chronicle.Latest; pass = latest != null && !string.IsNullOrEmpty(latest.DateLabel) && latest.WorldTime > 0; detail = $"date='{(latest != null ? latest.DateLabel : "")}' worldTime={(latest != null ? latest.WorldTime.ToString("0.###") : "0")} line='{(latest != null ? latest.Line : "")}'"; break; } case "caption_layout_ok": { float maxH = 250f; pass = WatchCaption.LastLayoutOk && WatchCaption.LastPanelSize.y > 20f && WatchCaption.LastPanelSize.y < maxH && WatchCaption.LastPanelSize.x <= 440f && (WatchCaption.LastHistoryShown <= 0 || WatchCaption.LastHistoryFillsBody) && WatchCaption.LastHistoryShown <= 3; detail = $"layoutOk={WatchCaption.LastLayoutOk} size={WatchCaption.LastPanelSize} histFill={WatchCaption.LastHistoryFillsBody} shown={WatchCaption.LastHistoryShown} caption='{(WatchCaption.LastCaptionText ?? "").Replace("\n", " | ")}' hist='{(WatchCaption.LastHistoryPreview ?? "").Replace("\n", " ")}'"; break; } case "dossier_traits_ok": { UnitDossier dossier = WatchCaption.Current; string preview = WatchCaption.LastTraitsPreview ?? ""; int count = dossier != null ? dossier.TopTraits.Count : 0; bool noEllipsis = preview.IndexOf("...", System.StringComparison.Ordinal) < 0; bool bounded = count >= 0 && count <= UnitDossier.MaxTraitChips; bool labelsMatch = true; if (dossier != null) { for (int i = 0; i < dossier.TopTraits.Count; i++) { string name = dossier.TopTraits[i].Name ?? ""; if (name.IndexOf("...", System.StringComparison.Ordinal) >= 0) { labelsMatch = false; break; } } } pass = bounded && noEllipsis && labelsMatch; detail = $"traits={count} preview='{preview}' layoutOk={WatchCaption.LastLayoutOk} size={WatchCaption.LastPanelSize}"; break; } case "dossier_statuses_ok": { UnitDossier dossier = WatchCaption.Current; string preview = WatchCaption.LastStatusesPreview ?? ""; int count = dossier != null ? dossier.TopStatuses.Count : 0; bool noEllipsis = preview.IndexOf("...", System.StringComparison.Ordinal) < 0; bool bounded = count >= 0 && count <= UnitDossier.MaxStatusChips; int wantMin = ParseCountExpect(cmd, defaultValue: 0); string cmp = (cmd.label ?? "min").Trim().ToLowerInvariant(); bool countOk = cmp == "exact" || cmp == "==" ? count == wantMin : count >= wantMin; pass = bounded && noEllipsis && countOk; detail = $"statuses={count} want={wantMin} cmp={cmp} preview='{preview}' layoutOk={WatchCaption.LastLayoutOk}"; break; } case "dossier_history_contains": { string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value; string hist = WatchCaption.LastHistoryJoined ?? ""; if (string.IsNullOrEmpty(hist)) { hist = WatchCaption.LastHistoryPreview ?? ""; } pass = !string.IsNullOrEmpty(needle) && hist.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0; detail = $"hist='{hist}' needle='{needle}' act={WatchCaption.LastActivityShown} life={WatchCaption.LastLifeShown}"; break; } case "dossier_history_not_contains": { string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value; string hist = WatchCaption.LastHistoryJoined ?? ""; if (string.IsNullOrEmpty(hist)) { hist = WatchCaption.LastHistoryPreview ?? ""; } pass = !string.IsNullOrEmpty(needle) && hist.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) < 0; detail = $"hist='{hist}' excludes='{needle}' act={WatchCaption.LastActivityShown} life={WatchCaption.LastLifeShown}"; break; } case "activity_count": { long id = WatchCaption.CurrentUnitId; int want = ParseCountExpect(cmd, defaultValue: 1); int have = ActivityLog.CountFor(id); string cmp = (cmd.label ?? "min").Trim().ToLowerInvariant(); pass = cmp == "exact" || cmp == "==" ? have == want : have >= want; detail = $"activity_count={have} want={want} id={id} shown={WatchCaption.LastActivityShown}"; break; } case "activity_prose_variety": { long id = WatchCaption.CurrentUnitId; IReadOnlyList entries = ActivityLog.LatestForSubject(id, 12); var distinct = new HashSet(StringComparer.Ordinal); for (int i = 0; i < entries.Count; i++) { if (entries[i] != null && !string.IsNullOrEmpty(entries[i].DisplayLine)) { distinct.Add(entries[i].DisplayLine); } } int want = ParseCountExpect(cmd, defaultValue: 2); pass = distinct.Count >= want; detail = $"distinct={distinct.Count} want={want} total={entries.Count}"; break; } case "activity_first_scoring": { pass = WorldActivityScanner.HarnessActivityFirstScoringOk(out string scoreDetail); detail = scoreDetail; break; } case "activity_names_colored": { long id = WatchCaption.CurrentUnitId; IReadOnlyList entries = ActivityLog.LatestForSubject(id, 8); bool any = false; bool colored = false; for (int i = 0; i < entries.Count; i++) { ActivityEntry e = entries[i]; if (e == null || string.IsNullOrEmpty(e.DisplayLineRich)) { continue; } any = true; if (e.DisplayLineRich.IndexOf("= 0) { colored = true; break; } } pass = any && colored; detail = $"any={any} colored={colored} preview='{WatchCaption.LastHistoryPreview}'"; break; } case "activity_log_contains": { long id = ResolveActivitySubjectId(); string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value; IReadOnlyList entries = ActivityLog.LatestForSubject(id, ActivityLog.MaxLinesPerSubject); bool hit = false; string sample = ""; for (int i = 0; i < entries.Count; i++) { ActivityEntry e = entries[i]; if (e == null) { continue; } string line = e.DisplayLine ?? e.Line ?? ""; if (string.IsNullOrEmpty(sample)) { sample = line; } if (!string.IsNullOrEmpty(needle) && line.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0) { hit = true; sample = line; break; } } pass = hit; detail = $"hit={hit} needle='{needle}' sample='{sample}' count={entries.Count} id={id}"; break; } case "activity_log_not_contains": { long id = ResolveActivitySubjectId(); string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value; IReadOnlyList entries = ActivityLog.LatestForSubject(id, ActivityLog.MaxLinesPerSubject); bool hit = false; string sample = ""; for (int i = 0; i < entries.Count; i++) { ActivityEntry e = entries[i]; if (e == null) { continue; } string line = e.DisplayLine ?? e.Line ?? ""; if (string.IsNullOrEmpty(sample)) { sample = line; } if (!string.IsNullOrEmpty(needle) && line.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0) { hit = true; sample = line; break; } } pass = !hit; detail = $"absent={!hit} needle='{needle}' sample='{sample}' count={entries.Count}"; break; } case "activity_verb_dedupe": { // Distinct play-like task ids should collapse to one Activity line within the cooldown. Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; if (focus == null || !focus.isAlive()) { pass = false; detail = "no_live_focus"; break; } ActivityLog.ResetSampleCounters(); string[] keys = { "task_unit_play", "child_play", "fun_move", "child_run_around", "frolic" }; int added = ActivityLog.HarnessSpamSameVerb(focus, keys); int suppressed = ActivityLog.VerbSuppressedSinceClear; pass = added == 1 && suppressed >= keys.Length - 1; detail = $"added={added} want_added=1 suppressed={suppressed} want_suppressed>={keys.Length - 1} keys={keys.Length}"; break; } case "activity_verb_exempt": { // Combat / high-signal verbs skip same-verb cooldown. Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; if (focus == null || !focus.isAlive()) { pass = false; detail = "no_live_focus"; break; } ActivityLog.ResetSampleCounters(); string[] keys = { "task_attack", "punch_a_building", "tantrum", "task_unit_attack" }; int added = ActivityLog.HarnessSpamSameVerb(focus, keys); int suppressed = ActivityLog.VerbSuppressedSinceClear; pass = added == keys.Length && suppressed == 0; detail = $"added={added} want_added={keys.Length} suppressed={suppressed} want_suppressed=0"; break; } case "activity_prose_sentence": { // Ensure mood/task labels become sentences, not "{name} laughing". long id = WatchCaption.CurrentUnitId; string actorName = ""; try { if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null) { actorName = MoveCamera._focus_unit.getName() ?? ""; } } catch { actorName = ""; } ActivityProse.Format( ActivityKind.TaskStart, "laughing", actorName, "", false, "", "Laughing", id, 99, out string plain, out _); bool ok = !string.IsNullOrEmpty(plain) && plain.IndexOf("laugh", System.StringComparison.OrdinalIgnoreCase) >= 0 && !plain.Equals( (string.IsNullOrEmpty(actorName) ? "Someone" : actorName) + " laughing", System.StringComparison.Ordinal); pass = ok; detail = $"plain='{plain}' actor='{actorName}'"; break; } case "activity_prose_alive": { // Generic tasks must read as living diary lines, not "{name} wanders". string actorName = "Dorraa"; ActivityProse.Format( ActivityKind.TaskStart, "random_move", ActivityContext.ForHarness(actorName, "", "buffalo"), "Wandering", 11, 1, out string moveLine, out _); ActivityProse.Format( ActivityKind.TaskStart, "wait", ActivityContext.ForHarness(actorName, "", "buffalo"), "Waiting", 12, 1, out string waitLine, out _); ActivityProse.Format( ActivityKind.TaskStart, "city_idle_walking", ActivityContext.ForHarness(actorName, "", "human", place: "Riverhold", jobId: "farmer"), "Walking", 13, 1, out string walkLine, out _); bool moveOk = !string.IsNullOrEmpty(moveLine) && moveLine.Length >= 28 && !moveLine.Equals(actorName + " wanders", System.StringComparison.Ordinal) && (moveLine.IndexOf("wander", System.StringComparison.OrdinalIgnoreCase) >= 0 || moveLine.IndexOf("drift", System.StringComparison.OrdinalIgnoreCase) >= 0 || moveLine.IndexOf("roam", System.StringComparison.OrdinalIgnoreCase) >= 0 || moveLine.IndexOf("ambl", System.StringComparison.OrdinalIgnoreCase) >= 0 || moveLine.IndexOf("trudg", System.StringComparison.OrdinalIgnoreCase) >= 0 || moveLine.IndexOf("move", System.StringComparison.OrdinalIgnoreCase) >= 0); bool waitOk = !string.IsNullOrEmpty(waitLine) && waitLine.Length >= 18 && !waitLine.Equals(actorName + " waits", System.StringComparison.Ordinal); bool walkOk = !string.IsNullOrEmpty(walkLine) && walkLine.IndexOf("Riverhold", System.StringComparison.OrdinalIgnoreCase) < 0 && walkLine.IndexOf("farmer", System.StringComparison.OrdinalIgnoreCase) < 0 && walkLine.IndexOf(", a ", System.StringComparison.OrdinalIgnoreCase) < 0 && walkLine.Length >= 28; pass = moveOk && waitOk && walkOk; detail = $"moveOk={moveOk} waitOk={waitOk} walkOk={walkOk} " + $"move='{moveLine}' wait='{waitLine}' walk='{walkLine}'"; break; } case "activity_prose_species": { // Species stays on the dossier nametag. Lines must diverge by manner / taxonomy // without spelling the species noun in the activity text. string actorName = "Tester"; try { if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null) { actorName = MoveCamera._focus_unit.getName() ?? "Tester"; } } catch { actorName = "Tester"; } ActivityProse.Format( ActivityKind.TaskStart, "task_unit_play", ActivityContext.ForHarness(actorName, "", "dog"), "Playing", 1, 1, out string dogLine, out _); ActivityProse.Format( ActivityKind.TaskStart, "task_unit_play", ActivityContext.ForHarness(actorName, "", "cat"), "Playing", 2, 1, out string catLine, out _); ActivityProse.Format( ActivityKind.TaskStart, "task_unit_play", ActivityContext.ForHarness(actorName, "", "human", place: "Riverhold"), "Playing", 3, 1, out string humanLine, out _); ActivityProse.Format( ActivityKind.ActionBeat, "BehUnloadResources", ActivityContext.ForHarness( actorName, "", "human", place: "Riverhold", carrying: "wheat"), "Unloads wheat", 4, 1, out string unloadLine, out _); // Species must never leak into the place clause (wild kingdoms often = species id). ActivityContext buffaloBadPlace = ActivityContext.ForHarness( "Dorraa", "", "buffalo", place: "buffalo"); buffaloBadPlace.PlaceLabel = ActivityInterestTable.PickPlaceLabel( "buffalo", "", "buffalo", "buffalo"); ActivityProse.Format( ActivityKind.TaskStart, "task_unit_play", buffaloBadPlace, "Playing", 5, 1, out string buffaloLine, out _); ActivityContext buffaloGoodPlace = ActivityContext.ForHarness( "Dorraa", "", "buffalo", place: "Riverhold"); ActivityProse.Format( ActivityKind.TaskStart, "task_unit_play", buffaloGoodPlace, "Playing", 6, 1, out string buffaloTownLine, out _); bool dogOk = !string.IsNullOrEmpty(dogLine) && dogLine.Length >= 16 && dogLine.IndexOf(" the dog", System.StringComparison.OrdinalIgnoreCase) < 0; bool catOk = !string.IsNullOrEmpty(catLine) && catLine.Length >= 16 && catLine.IndexOf(" the cat", System.StringComparison.OrdinalIgnoreCase) < 0; bool humanOk = !string.IsNullOrEmpty(humanLine) && !humanLine.Equals(dogLine, System.StringComparison.Ordinal) && !humanLine.Equals(catLine, System.StringComparison.Ordinal) && humanLine.IndexOf(" the human", System.StringComparison.OrdinalIgnoreCase) < 0; bool unloadOk = !string.IsNullOrEmpty(unloadLine) && unloadLine.IndexOf("wheat", System.StringComparison.OrdinalIgnoreCase) >= 0 && unloadLine.IndexOf("Riverhold", System.StringComparison.OrdinalIgnoreCase) >= 0 && unloadLine.IndexOf("while carrying", System.StringComparison.OrdinalIgnoreCase) < 0; bool distinct = !dogLine.Equals(catLine, System.StringComparison.Ordinal); bool placeRejectOk = !string.IsNullOrEmpty(buffaloLine) && buffaloLine.IndexOf(" in buffalo", System.StringComparison.OrdinalIgnoreCase) < 0 && buffaloLine.IndexOf(" the buffalo", System.StringComparison.OrdinalIgnoreCase) < 0; // Play is not place-centric - city must not appear on play lines. bool placeKeepOk = !string.IsNullOrEmpty(buffaloTownLine) && buffaloTownLine.IndexOf("Riverhold", System.StringComparison.OrdinalIgnoreCase) < 0 && humanLine.IndexOf("Riverhold", System.StringComparison.OrdinalIgnoreCase) < 0; ActivityProse.Format( ActivityKind.ActionBeat, "BehSocializeTalk", ActivityContext.ForHarness(actorName, "Tomi", "human", place: "Celuford"), "Talks", 11, 1, out string socialLine, out _); bool socialBindOk = !string.IsNullOrEmpty(socialLine) && socialLine.IndexOf("Tomi", System.StringComparison.OrdinalIgnoreCase) >= 0 && socialLine.IndexOf("with another", System.StringComparison.OrdinalIgnoreCase) < 0 && socialLine.IndexOf("nearby company", System.StringComparison.OrdinalIgnoreCase) < 0; int withTomi = 0; int withIdx = 0; while (withIdx >= 0) { withIdx = socialLine.IndexOf( "with Tomi", withIdx, System.StringComparison.OrdinalIgnoreCase); if (withIdx < 0) { break; } withTomi++; withIdx += "with Tomi".Length; } socialBindOk = socialBindOk && withTomi <= 1; // Building/place labels must never become companion "with …" on work/unload. // Objects use "at the …"; settlements use "in …". ActivityContext bonfireCtx = ActivityContext.ForHarness( actorName, "", "human", place: "bonfire", placeIsObject: true); ActivityProse.Format( ActivityKind.TaskStart, "type_bonfire", bonfireCtx, "Works a bonfire", 12, 1, out string bonfireLine, out _); bool bonfireOk = !string.IsNullOrEmpty(bonfireLine) && bonfireLine.IndexOf("at the bonfire", System.StringComparison.OrdinalIgnoreCase) >= 0 && bonfireLine.IndexOf("with bonfire", System.StringComparison.OrdinalIgnoreCase) < 0 && bonfireLine.IndexOf("in bonfire", System.StringComparison.OrdinalIgnoreCase) < 0; ActivityContext unloadTownCtx = ActivityContext.ForHarness( actorName, "", "human", place: "Riverhold", carrying: "wheat"); ActivityProse.Format( ActivityKind.ActionBeat, "BehUnloadResources", unloadTownCtx, "Unloads wheat", 13, 1, out string unloadTownLine, out _); bool unloadTownOk = !string.IsNullOrEmpty(unloadTownLine) && unloadTownLine.IndexOf("wheat", System.StringComparison.OrdinalIgnoreCase) >= 0 && unloadTownLine.IndexOf("in Riverhold", System.StringComparison.OrdinalIgnoreCase) >= 0 && unloadTownLine.IndexOf("with town", System.StringComparison.OrdinalIgnoreCase) < 0; // Taxonomy kingdoms like "insect" must never become place clauses. string insectPlace = ActivityInterestTable.PickPlaceLabel( "insect", "", "insect", "beetle"); ActivityContext beetleCtx = ActivityContext.ForHarness("Eeeew", "", "beetle", place: insectPlace); beetleCtx.PlaceLabel = insectPlace; ActivityProse.Format( ActivityKind.TaskStart, "task_unit_play", beetleCtx, "Playing", 7, 1, out string beetleLine, out _); bool insectRejectOk = string.IsNullOrEmpty(insectPlace) && !string.IsNullOrEmpty(beetleLine) && beetleLine.IndexOf(" in insect", System.StringComparison.OrdinalIgnoreCase) < 0 && beetleLine.IndexOf(" the beetle", System.StringComparison.OrdinalIgnoreCase) < 0; ActivityProse.Format( ActivityKind.TaskStart, "task_unit_play", ActivityContext.ForHarness("Creaker", "", "frog"), "Playing", 8, 1, out string frogLine, out _); ActivityProse.Format( ActivityKind.TaskStart, "child_random_jump", ActivityContext.ForHarness("Creaker", "", "frog"), "Jumping", 9, 1, out string frogJumpLine, out _); bool frogOk = !string.IsNullOrEmpty(frogLine) && frogLine.Length >= 16 && frogLine.IndexOf(" the frog", System.StringComparison.OrdinalIgnoreCase) < 0 && (frogLine.IndexOf("hop", System.StringComparison.OrdinalIgnoreCase) >= 0 || frogLine.IndexOf("leap", System.StringComparison.OrdinalIgnoreCase) >= 0 || frogLine.IndexOf("bounc", System.StringComparison.OrdinalIgnoreCase) >= 0 || frogLine.IndexOf("splash", System.StringComparison.OrdinalIgnoreCase) >= 0 || frogLine.IndexOf("play", System.StringComparison.OrdinalIgnoreCase) >= 0); bool frogJumpOk = !string.IsNullOrEmpty(frogJumpLine) && frogJumpLine.Length >= 16 && frogJumpLine.IndexOf(" the frog", System.StringComparison.OrdinalIgnoreCase) < 0 && frogJumpLine.IndexOf("young", System.StringComparison.OrdinalIgnoreCase) >= 0; // Live ActorAsset taxonomy - not curated id maps. ActivityContext frogCtx = ActivityContext.ForHarness("Creaker", "", "frog"); ActivityContext dogCtxLive = ActivityContext.ForHarness(actorName, "", "dog"); bool liveTaxOk = ActivityAssetCatalog.IsLiveActorAssetId("frog") && frogCtx.Voice.BaseBodyTag == ActivityBodyTag.Amphibian && frogCtx.TaxonomicClass.Equals( "amphibia", System.StringComparison.OrdinalIgnoreCase) && dogCtxLive.Family == ActivityVoiceFamilies.Canine && dogCtxLive.TaxonomicFamily.Equals( "canidae", System.StringComparison.OrdinalIgnoreCase); // Species with no polish override still gets taxonomy manner without naming itself. ActivityProse.Format( ActivityKind.TaskStart, "task_unit_play", ActivityContext.ForHarness("Horn", "", "rhino"), "Playing", 10, 1, out string rhinoLine, out _); bool rhinoOk = !string.IsNullOrEmpty(rhinoLine) && rhinoLine.Length >= 16 && rhinoLine.IndexOf(" the rhino", System.StringComparison.OrdinalIgnoreCase) < 0; pass = dogOk && catOk && humanOk && unloadOk && distinct && placeRejectOk && placeKeepOk && insectRejectOk && frogOk && frogJumpOk && liveTaxOk && rhinoOk && socialBindOk && bonfireOk && unloadTownOk; detail = $"dogOk={dogOk} catOk={catOk} humanOk={humanOk} unloadOk={unloadOk} distinct={distinct} " + $"placeRejectOk={placeRejectOk} placeKeepOk={placeKeepOk} insectRejectOk={insectRejectOk} " + $"frogOk={frogOk} frogJumpOk={frogJumpOk} liveTaxOk={liveTaxOk} rhinoOk={rhinoOk} " + $"socialBindOk={socialBindOk} bonfireOk={bonfireOk} unloadTownOk={unloadTownOk} " + $"frogFamily={frogCtx.Family} frogClass={frogCtx.TaxonomicClass} " + $"dogFamily={dogCtxLive.Family} dogTaxFamily={dogCtxLive.TaxonomicFamily} " + $"dog='{dogLine}' cat='{catLine}' human='{humanLine}' unload='{unloadLine}' " + $"buffalo='{buffaloLine}' buffaloTown='{buffaloTownLine}' beetle='{beetleLine}' " + $"frog='{frogLine}' frogJump='{frogJumpLine}' rhino='{rhinoLine}' social='{socialLine}' " + $"bonfire='{bonfireLine}' unloadTown='{unloadTownLine}'"; break; } case "activity_taxonomy_audit": { ActivityVoiceAuditResult audit = ActivityVoiceHarness.RunAudit(HarnessDir()); pass = audit.Passed; detail = audit.Detail; break; } case "activity_status_audit": { ActivityStatusAuditResult audit = ActivityStatusHarness.RunAudit(HarnessDir()); pass = audit.Passed; detail = audit.Detail; break; } case "activity_happiness_audit": { ActivityHappinessAuditResult audit = ActivityHappinessHarness.RunAudit(HarnessDir()); pass = audit.Passed; detail = audit.Detail; break; } case "world_log_audit": { WorldLogEventAuditResult audit = WorldLogEventHarness.RunAudit(HarnessDir()); pass = audit.Passed; detail = audit.Detail; break; } case "disaster_war_inventory": case "disaster_war_audit": { DisasterWarAuditResult audit = DisasterWarHarness.RunAudit(HarnessDir()); WorldLogEventHarness.DumpDisasterWarInventory(HarnessDir()); pass = audit.Passed; detail = audit.Detail; break; } case "domain_event_audit": { DomainEventAuditResult audit = DomainEventHarness.RunAudit(HarnessDir()); pass = audit.Passed; detail = audit.Detail; break; } case "event_inject_coverage": { Actor focus = ResolveUnit(null) ?? (MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null) ?? WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f); EventInjectCoverageResult coverage = EventInjectCoverageHarness.Run(HarnessDir(), focus); pass = coverage.Passed; detail = coverage.Detail; break; } case "event_live_apply_loop": { Actor focus = ResolveUnit(null) ?? (MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null) ?? WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f); EventLiveApplyLoopResult loop = EventLiveApplyLoopHarness.Run(HarnessDir(), focus); pass = loop.Passed; detail = loop.Detail; break; } case "event_live_relationship": { Actor focus = ResolveUnit(null) ?? (MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null) ?? WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f); EventLiveRelationshipResult rel = EventLiveRelationshipHarness.Run(HarnessDir(), focus); pass = rel.Passed; detail = rel.Detail; break; } case "event_live_pipelines": { Actor focus = ResolveUnit(null) ?? (MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null) ?? WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f); EventLivePipelinesResult pipes = EventLivePipelinesHarness.Run(HarnessDir(), focus); pass = pipes.Passed; detail = pipes.Detail; break; } case "event_live_coverage": { // Report-only: seed if empty, write TSV, fail only on live_level=fail. if (EventLiveCoverageLedger.RowCount == 0) { EventLiveCoverageLedger.SeedFromCatalogs(); } string summary = EventLiveCoverageLedger.Write(HarnessDir()); EventLiveCoverageSummary counts = EventLiveCoverageLedger.Summarize(); pass = counts.Fail == 0; detail = summary; break; } case "mutation_discovery": { MutationDiscoveryResult discovery = MutationDiscoveryHarness.Run(HarnessDir()); pass = discovery.Passed; detail = discovery.Detail; break; } case "activity_happiness_count": { int want = ParseCountExpect(cmd, defaultValue: 1); int got = HappinessEventRouter.NotesSinceClear; string mode = (cmd.label ?? "exact").Trim().ToLowerInvariant(); pass = mode == "min" ? got >= want : got == want; detail = $"notes={got} want={want} mode={mode} last={HappinessEventRouter.LastEffectId}"; break; } case "activity_happiness_ring_count": { int want = ParseCountExpect(cmd, defaultValue: 1); int got = HappinessEventRouter.RingWritesSinceClear; string mode = (cmd.label ?? "exact").Trim().ToLowerInvariant(); pass = mode == "min" ? got >= want : got == want; detail = $"ring={got} want={want} mode={mode}"; break; } case "activity_happiness_life_count": { int want = ParseCountExpect(cmd, defaultValue: 1); int got = HappinessEventRouter.LifeWritesSinceClear; string mode = (cmd.label ?? "exact").Trim().ToLowerInvariant(); pass = mode == "min" ? got >= want : got == want; detail = $"life={got} want={want} mode={mode}"; break; } case "activity_happiness_task_id": { long id = ResolveActivitySubjectId(); string want = cmd.value ?? ""; string got = ""; IReadOnlyList list = ActivityLog.LatestForSubject(id, 1); if (list != null && list.Count > 0 && list[0] != null) { got = list[0].TaskId ?? ""; } pass = !string.IsNullOrEmpty(want) && got.Equals(want, StringComparison.OrdinalIgnoreCase); detail = $"got='{got}' want='{want}' id={id}"; break; } case "activity_happiness_merged": { int want = ParseCountExpect(cmd, defaultValue: 1); int got = HappinessEventRouter.MergedSinceClear; string mode = (cmd.label ?? "min").Trim().ToLowerInvariant(); pass = mode == "exact" ? got == want : got >= want; detail = $"merged={got} want={want} mode={mode}"; break; } case "activity_status_gain_count": { int want = ParseCountExpect(cmd, defaultValue: 1); string statusId = cmd.asset ?? ""; int got = string.IsNullOrEmpty(statusId) ? ActivityLog.StatusGainsSinceClear : ActivityLog.StatusGainCount(statusId); string mode = (cmd.label ?? "exact").Trim().ToLowerInvariant(); pass = mode == "min" ? got >= want : got == want; detail = $"gains={got} want={want} mode={mode} status={statusId}" + $" total={ActivityLog.StatusGainsSinceClear} last={ActivityLog.LastStatusGainId}"; break; } case "activity_status_loss_count": { int want = ParseCountExpect(cmd, defaultValue: 1); int got = ActivityLog.StatusLossesSinceClear; string mode = (cmd.label ?? "exact").Trim().ToLowerInvariant(); pass = mode == "min" ? got >= want : got == want; detail = $"losses={got} want={want} mode={mode} last={ActivityLog.LastStatusLossId}"; break; } case "activity_status_refresh_count": { int want = ParseCountExpect(cmd, defaultValue: 1); int got = ActivityLog.StatusRefreshesSinceClear; string mode = (cmd.label ?? "exact").Trim().ToLowerInvariant(); pass = mode == "min" ? got >= want : got == want; detail = $"refreshes={got} want={want} mode={mode}"; break; } case "activity_status_active": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; string statusId = cmd.value ?? ""; bool expectActive = true; string rawExpect = (cmd.label ?? cmd.expect ?? "true").Trim().ToLowerInvariant(); if (rawExpect == "false" || rawExpect == "0" || rawExpect == "no") { expectActive = false; } bool active = StatusGameApi.HasStatus(focus, statusId); pass = active == expectActive; detail = $"status={statusId} active={active} expectActive={expectActive}"; break; } case "activity_status_task_id": { long id = WatchCaption.CurrentUnitId; if (id == 0 && MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null) { try { id = MoveCamera._focus_unit.getID(); } catch { id = 0; } } string want = cmd.value ?? ""; string got = ""; bool found = false; IReadOnlyList list = ActivityLog.LatestForSubject(id, 12); if (list != null) { for (int i = 0; i < list.Count; i++) { string taskId = list[i]?.TaskId ?? ""; if (i == 0) { got = taskId; } if (taskId.Equals(want, StringComparison.OrdinalIgnoreCase)) { found = true; break; } } } pass = !string.IsNullOrEmpty(want) && found; detail = $"latest='{got}' found={found} want='{want}'"; break; } case "activity_prose_showcase": { // Taxonomy must map to real buckets - never fantasy kingdoms -> monster. string[] speciesIds = { "zombie", "angle", "lemon_snail", "dragon", "fairy" }; string[] wantFamily = { ActivityVoiceFamilies.Mammal, ActivityVoiceFamilies.Mythic, ActivityVoiceFamilies.Plant, ActivityVoiceFamilies.Mythic, ActivityVoiceFamilies.Mythic }; ActivityBodyTag[] wantManner = { ActivityBodyTag.Undead, ActivityBodyTag.Mythic, ActivityBodyTag.Plant, ActivityBodyTag.Dragon, ActivityBodyTag.Insect }; var verbs = new (string key, string fact, bool withTarget)[] { ("task_unit_play", "Playing", false), ("move", "Moving", false), ("task_hunt", "Hunting", true), ("task_sleep", "Sleeping", false), ("task_eat", "Eating", false), ("laugh", "Laughing", false), ("task_attack", "Fighting", true) }; var dump = new StringBuilder(2048); int lineNo = 0; int okLines = 0; int leakLines = 0; int classOk = 0; for (int s = 0; s < speciesIds.Length; s++) { string sid = speciesIds[s]; ActivityContext probe = ActivityContext.ForHarness("Tester", "", sid); bool mapped = probe.Family == wantFamily[s] && probe.Voice.EffectiveActionTag == wantManner[s]; if (mapped) { classOk++; } dump.AppendLine( "=== " + sid + " label='" + probe.SpeciesLabel + "'" + " tax='" + probe.TaxonomicKingdom + "'" + " family=" + probe.Family + " manner=" + probe.MannerTag + " wantFamily=" + wantFamily[s] + " wantManner=" + wantManner[s] + " mapped=" + mapped + " live=" + ActivityAssetCatalog.IsLiveActorAssetId(sid) + " ==="); for (int v = 0; v < verbs.Length; v++) { string key = verbs[v].key; string fact = verbs[v].fact; bool withTarget = verbs[v].withTarget; lineNo++; string target = withTarget ? "Prey" : ""; ActivityContext ctx = ActivityContext.ForHarness("Tester", target, sid); ActivityProse.Format( ActivityKind.TaskStart, key, ctx, fact, 9000 + s, lineNo, out string plain, out _); bool leak = !string.IsNullOrEmpty(probe.SpeciesLabel) && plain.IndexOf( " the " + probe.SpeciesLabel, StringComparison.OrdinalIgnoreCase) >= 0; if (leak) { leakLines++; } else if (!string.IsNullOrEmpty(plain) && plain.Length >= 8) { okLines++; } dump.AppendLine(" " + key + ": " + plain); Debug.Log( "[IdleSpectator][SHOWCASE] " + sid + "|" + key + "|" + probe.MannerTag + "|" + plain); } } int kingdomFail = 0; string kingdomFailSample = ""; List actors = ActivityAssetCatalog.EnumerateLiveActorAssetIds(); for (int i = 0; i < actors.Count; i++) { ActorAsset asset = ActivityAssetCatalog.TryGetActorAsset(actors[i]); if (asset == null || asset.civ) { continue; } string tax = asset.name_taxonomic_kingdom ?? ""; if (string.IsNullOrEmpty(tax)) { continue; } string fam = ActivityVoiceResolver.Resolve(asset).BaseFamily; bool fantasyKingdom = tax.Equals("plantae", StringComparison.OrdinalIgnoreCase) || tax.Equals("fungi", StringComparison.OrdinalIgnoreCase) || tax.Equals("protista", StringComparison.OrdinalIgnoreCase) || tax.Equals("machina", StringComparison.OrdinalIgnoreCase) || tax.Equals("mythoria", StringComparison.OrdinalIgnoreCase) || tax.Equals("gregalia", StringComparison.OrdinalIgnoreCase); if (fantasyKingdom && fam == ActivityVoiceFamilies.Default) { kingdomFail++; if (kingdomFailSample.Length < 180) { if (kingdomFailSample.Length > 0) { kingdomFailSample += "; "; } kingdomFailSample += actors[i] + ":" + tax + "->" + fam; } } } string path = Path.Combine(HarnessDir(), "activity-prose-showcase.txt"); try { File.WriteAllText(path, dump.ToString()); } catch (Exception ex) { Debug.LogWarning("[IdleSpectator][SHOWCASE] write failed: " + ex.Message); } pass = okLines >= 20 && leakLines == 0 && classOk == speciesIds.Length && kingdomFail == 0; detail = "okLines=" + okLines + " leakLines=" + leakLines + " classOk=" + classOk + " kingdomFail=" + kingdomFail + " kingdomFailSample='" + kingdomFailSample + "'" + " path=" + path; break; } case "activity_prose_grounding": { // Same verb, different observed clauses must diverge and mention those facts. const string verb = "BehUnloadResources"; ActivityProse.Format( ActivityKind.ActionBeat, verb, ActivityContext.ForHarness( "Mira", "", "human", place: "Riverhold", carrying: "wheat", jobId: "farmer", traitId: "curious", isChild: true), "Unloads wheat", 101, 1, out string farmerLine, out _); ActivityProse.Format( ActivityKind.ActionBeat, verb, ActivityContext.ForHarness( "Mira", "", "human", place: "Ironford", carrying: "stone", jobId: "miner", traitId: "brave"), "Unloads stone", 102, 1, out string minerLine, out _); ActivityProse.Format( ActivityKind.TaskStart, "task_unit_play", ActivityContext.ForHarness( "Mira", "Barkley", "human", place: "Riverhold", jobId: "farmer", isKing: true, inCombat: true), "Playing", 103, 1, out string kingCombatLine, out _); ActivityProse.Format( ActivityKind.TaskStart, "task_hunt", ActivityContext.ForHarness( "Gogith", "Boortkin", "human", place: "Thasthuk", isKing: true, inCombat: true), "Hunting", 107, 1, out string huntCombatLine, out _); bool farmerOk = !string.IsNullOrEmpty(farmerLine) && farmerLine.IndexOf("young", System.StringComparison.OrdinalIgnoreCase) >= 0 && farmerLine.IndexOf("farmer", System.StringComparison.OrdinalIgnoreCase) < 0 && farmerLine.IndexOf(", a ", System.StringComparison.OrdinalIgnoreCase) < 0 && farmerLine.IndexOf("Riverhold", System.StringComparison.OrdinalIgnoreCase) >= 0 && farmerLine.IndexOf("wheat", System.StringComparison.OrdinalIgnoreCase) >= 0 && farmerLine.IndexOf("while carrying", System.StringComparison.OrdinalIgnoreCase) < 0 && farmerLine.IndexOf(" the human", System.StringComparison.OrdinalIgnoreCase) < 0 && farmerLine.IndexOf("curious", System.StringComparison.OrdinalIgnoreCase) < 0; bool minerOk = !string.IsNullOrEmpty(minerLine) && minerLine.IndexOf("miner", System.StringComparison.OrdinalIgnoreCase) < 0 && minerLine.IndexOf(", a ", System.StringComparison.OrdinalIgnoreCase) < 0 && minerLine.IndexOf("Ironford", System.StringComparison.OrdinalIgnoreCase) >= 0 && minerLine.IndexOf("stone", System.StringComparison.OrdinalIgnoreCase) >= 0 && minerLine.IndexOf("while carrying", System.StringComparison.OrdinalIgnoreCase) < 0 && minerLine.IndexOf("brave", System.StringComparison.OrdinalIgnoreCase) < 0; bool divergeOk = !farmerLine.Equals(minerLine, System.StringComparison.Ordinal); // Quiet kings: role stays off Activity; combat / target pressure still surfaces. bool kingOk = !string.IsNullOrEmpty(kingCombatLine) && kingCombatLine.IndexOf("king", System.StringComparison.OrdinalIgnoreCase) < 0 && kingCombatLine.IndexOf("Riverhold", System.StringComparison.OrdinalIgnoreCase) < 0 && (kingCombatLine.IndexOf("combat", System.StringComparison.OrdinalIgnoreCase) >= 0 || kingCombatLine.IndexOf("keeping", System.StringComparison.OrdinalIgnoreCase) >= 0 || kingCombatLine.IndexOf("Barkley", System.StringComparison.OrdinalIgnoreCase) >= 0); bool huntPressureOk = !string.IsNullOrEmpty(huntCombatLine) && huntCombatLine.IndexOf("Boortkin", System.StringComparison.OrdinalIgnoreCase) >= 0 && huntCombatLine.IndexOf("Thasthuk", System.StringComparison.OrdinalIgnoreCase) < 0 && huntCombatLine.IndexOf("king", System.StringComparison.OrdinalIgnoreCase) < 0 && huntCombatLine.IndexOf( "under combat pressure", System.StringComparison.OrdinalIgnoreCase) < 0; // Taxonomy manner divergence without SpeciesOverrides (rhino vs beetle vs frog). ActivityProse.Format( ActivityKind.TaskStart, "task_unit_play", ActivityContext.ForHarness("A", "", "beetle"), "Playing", 104, 1, out string beetlePlay, out _); ActivityProse.Format( ActivityKind.TaskStart, "task_unit_play", ActivityContext.ForHarness("B", "", "frog"), "Playing", 105, 1, out string frogPlay, out _); ActivityProse.Format( ActivityKind.TaskStart, "task_unit_play", ActivityContext.ForHarness("C", "", "rhino"), "Playing", 106, 1, out string rhinoPlay, out _); ActivityProse.Format( ActivityKind.TaskStart, "task_unit_play", ActivityContext.ForHarness( "Crob", "", "crab", terrain: ActivityTerrain.Land), "Playing", 108, 1, out string crabLandPlay, out _); ActivityProse.Format( ActivityKind.TaskStart, "task_unit_play", ActivityContext.ForHarness( "Pip", "", "piranha", terrain: ActivityTerrain.Land), "Playing", 109, 1, out string fishLandPlay, out _); ActivityProse.Format( ActivityKind.TaskStart, "task_unit_play", ActivityContext.ForHarness( "Pip", "", "piranha", terrain: ActivityTerrain.Liquid), "Playing", 109, 1, out string fishWaterPlay, out _); bool taxDiverge = !string.IsNullOrEmpty(beetlePlay) && !string.IsNullOrEmpty(frogPlay) && !string.IsNullOrEmpty(rhinoPlay) && !beetlePlay.Equals(frogPlay, System.StringComparison.Ordinal) && !frogPlay.Equals(rhinoPlay, System.StringComparison.Ordinal) && beetlePlay.IndexOf(" the beetle", System.StringComparison.OrdinalIgnoreCase) < 0 && frogPlay.IndexOf(" the frog", System.StringComparison.OrdinalIgnoreCase) < 0 && rhinoPlay.IndexOf(" the rhino", System.StringComparison.OrdinalIgnoreCase) < 0 && (beetlePlay.IndexOf("leg", System.StringComparison.OrdinalIgnoreCase) >= 0 || beetlePlay.IndexOf("rocks", System.StringComparison.OrdinalIgnoreCase) >= 0 || beetlePlay.IndexOf("wing", System.StringComparison.OrdinalIgnoreCase) >= 0) && (frogPlay.IndexOf("hop", System.StringComparison.OrdinalIgnoreCase) >= 0 || frogPlay.IndexOf("bounc", System.StringComparison.OrdinalIgnoreCase) >= 0 || frogPlay.IndexOf("pops", System.StringComparison.OrdinalIgnoreCase) >= 0 || frogPlay.IndexOf("toe", System.StringComparison.OrdinalIgnoreCase) >= 0) && (rhinoPlay.IndexOf("horn", System.StringComparison.OrdinalIgnoreCase) >= 0 || rhinoPlay.IndexOf("stout", System.StringComparison.OrdinalIgnoreCase) >= 0 || rhinoPlay.IndexOf("buck", System.StringComparison.OrdinalIgnoreCase) >= 0 || rhinoPlay.IndexOf("pivot", System.StringComparison.OrdinalIgnoreCase) >= 0); bool crabLandOk = !string.IsNullOrEmpty(crabLandPlay) && crabLandPlay.IndexOf("water", System.StringComparison.OrdinalIgnoreCase) < 0 && crabLandPlay.IndexOf("wet", System.StringComparison.OrdinalIgnoreCase) < 0 && (crabLandPlay.IndexOf("claw", System.StringComparison.OrdinalIgnoreCase) >= 0 || crabLandPlay.IndexOf("pincer", System.StringComparison.OrdinalIgnoreCase) >= 0 || crabLandPlay.IndexOf("side", System.StringComparison.OrdinalIgnoreCase) >= 0); bool fishTerrainOk = !string.IsNullOrEmpty(fishLandPlay) && !string.IsNullOrEmpty(fishWaterPlay) && fishLandPlay.IndexOf("water", System.StringComparison.OrdinalIgnoreCase) < 0 && fishLandPlay.IndexOf("wet", System.StringComparison.OrdinalIgnoreCase) < 0 && fishWaterPlay.IndexOf("water", System.StringComparison.OrdinalIgnoreCase) >= 0; pass = farmerOk && minerOk && divergeOk && kingOk && huntPressureOk && taxDiverge && crabLandOk && fishTerrainOk; detail = $"farmerOk={farmerOk} minerOk={minerOk} divergeOk={divergeOk} kingOk={kingOk} " + $"huntPressureOk={huntPressureOk} taxDiverge={taxDiverge} " + $"crabLandOk={crabLandOk} fishTerrainOk={fishTerrainOk} " + $"farmer='{farmerLine}' miner='{minerLine}' king='{kingCombatLine}' hunt='{huntCombatLine}' " + $"beetle='{beetlePlay}' frog='{frogPlay}' rhino='{rhinoPlay}' " + $"crabLand='{crabLandPlay}' fishLand='{fishLandPlay}' fishWater='{fishWaterPlay}'"; break; } case "activity_prose_library": { // Coverage from live libraries - not curated shortlists. List actors = ActivityAssetCatalog.EnumerateLiveActorAssetIds(); List tasks = ActivityVerbMap.EnumerateLiveTaskIds(); int actorFail = 0; int actorChecked = 0; string actorFailSample = ""; for (int i = 0; i < actors.Count; i++) { string sid = actors[i]; ActorAsset asset = ActivityAssetCatalog.TryGetActorAsset(sid); if (asset == null) { continue; } // Stratified sample: every 3rd asset, plus always dog/cat/frog/beetle/rhino/human. bool force = sid == "dog" || sid == "cat" || sid == "frog" || sid == "beetle" || sid == "rhino" || sid == "human"; if (!force && (i % 3) != 0) { continue; } actorChecked++; ActivityContext ctx = ActivityContext.ForHarness("Lib", "", sid); ActivityProse.Format( ActivityKind.TaskStart, "task_unit_play", ctx, "Playing", 200 + i, 1, out string line, out _); bool humanoid = ctx.IsCiv && ctx.IsHumanoid; bool ok = !string.IsNullOrEmpty(line) && line.Length >= 18; // Dossier shows species - activity lines must not spell "the {species}". string label = string.IsNullOrEmpty(ctx.SpeciesLabel) ? sid : ctx.SpeciesLabel; if (!string.IsNullOrEmpty(label)) { ok = ok && line.IndexOf(" the " + label, System.StringComparison.OrdinalIgnoreCase) < 0; } if (humanoid) { ok = ok && line.IndexOf(" the human", System.StringComparison.OrdinalIgnoreCase) < 0; } if (!ok) { actorFail++; if (string.IsNullOrEmpty(actorFailSample)) { actorFailSample = sid + ":'" + line + "'"; } else if (actorFail <= 6 && actorFailSample.Length < 220) { actorFailSample += "; " + sid + ":'" + line + "'"; } } } int taskFail = 0; int taskChecked = 0; string taskFailSample = ""; // Prefer live task library; fall back to snapshot file ids if library empty. List taskIds = tasks; if (taskIds.Count == 0) { try { string path = System.IO.Path.Combine(ModClass.ModFolder, ".harness", "actor_task_ids.txt"); if (System.IO.File.Exists(path)) { foreach (string line in System.IO.File.ReadAllLines(path)) { if (!string.IsNullOrEmpty(line)) { taskIds.Add(line.Trim()); } } } } catch { // ignore } } for (int i = 0; i < taskIds.Count; i++) { string tid = taskIds[i]; if (string.IsNullOrEmpty(tid)) { continue; } taskChecked++; if (!ActivityVerbMap.HasActionCore(tid)) { taskFail++; if (string.IsNullOrEmpty(taskFailSample)) { taskFailSample = tid + ":no-core"; } continue; } ActivityProse.Format( ActivityKind.TaskStart, tid, ActivityContext.ForHarness("Lib", "", "dog", taskKey: tid), tid, 300 + i, 1, out string line, out _); bool ok = !string.IsNullOrEmpty(line) && line.Length >= 16 && !line.Equals("Lib " + tid, System.StringComparison.OrdinalIgnoreCase) && line.IndexOf("Lib", System.StringComparison.Ordinal) >= 0; // Reject bare gerund dead ends like "Lib laughing" string[] parts = line.Split(new[] { ' ' }, 3); if (parts.Length == 2 && parts[1].EndsWith("ing", System.StringComparison.OrdinalIgnoreCase)) { ok = false; } if (!ok) { taskFail++; if (taskFailSample.IndexOf(tid, System.StringComparison.Ordinal) < 0 && taskFailSample.Length < 120) { taskFailSample += (string.IsNullOrEmpty(taskFailSample) ? "" : ";") + tid + ":'" + line + "'"; } } } bool actorsOk = actors.Count >= 20 && actorFail == 0 && actorChecked >= 10; bool tasksOk = taskChecked >= 50 && taskFail == 0; pass = actorsOk && tasksOk; detail = $"actors={actors.Count} actorChecked={actorChecked} actorFail={actorFail} " + $"tasksLive={tasks.Count} taskChecked={taskChecked} taskFail={taskFail} " + $"actorFailSample='{actorFailSample}' taskFailSample='{taskFailSample}'"; break; } case "lore_detail_pane": { string want = (cmd.value ?? "life").Trim().ToLowerInvariant(); ChronicleHud.DetailPane have = ChronicleHud.ActiveDetailPane; bool match = want.StartsWith("act") ? have == ChronicleHud.DetailPane.Activity : have == ChronicleHud.DetailPane.Life; pass = Chronicle.HudVisible && ChronicleHud.DetailUnitId != 0 && match; detail = $"pane={have} want={want} actRows={ChronicleHud.LastDetailActivityRows} lifeRows={ChronicleHud.LastDetailHistoryRows}"; break; } case "lore_activity_shown": { int want = ParseCountExpect(cmd, defaultValue: 1); int have = ChronicleHud.LastDetailActivityRows; string cmp = (cmd.label ?? "min").Trim().ToLowerInvariant(); pass = Chronicle.HudVisible && ChronicleHud.ActiveDetailPane == ChronicleHud.DetailPane.Activity && (cmp == "exact" || cmp == "==" ? have == want : have >= want); detail = $"actRows={have} want={want} pane={ChronicleHud.ActiveDetailPane}"; break; } case "activity_band_cold_focus": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; ActivityBand band = ActivityInterestTable.Classify(focus); bool cold = WorldActivityScanner.IsFocusActivityCold(focus); string want = (cmd.value ?? "cold").Trim().ToLowerInvariant(); pass = want == "cold" ? cold : !cold; detail = $"band={band} cold={cold} want={want} probe={ActivityInterestTable.DumpFocusProbe(focus)}"; break; } case "chronicle_world_latest_contains": case "world_memory_contains": { string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value; ChronicleEntry latest = Chronicle.LatestWorld; string display = latest != null ? latest.DisplayLine : ""; pass = false; if (!string.IsNullOrEmpty(needle)) { IReadOnlyList snap = Chronicle.SnapshotMemory(); for (int i = 0; i < snap.Count; i++) { ChronicleEntry e = snap[i]; if (e == null || e.Kind == ChronicleKind.AgeChapter) { continue; } if (e.Kind != ChronicleKind.World) { continue; } string line = e.Line ?? ""; string lore = e.HudLine ?? ""; if (line.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0 || lore.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0) { pass = true; display = e.DisplayLine ?? display; break; } } } detail = $"world_hit='{display}' needle='{needle}' age='{Chronicle.CurrentAgeName}' latest='{(latest != null ? latest.DisplayLine : "")}'"; break; } case "world_memory_age": { pass = !string.IsNullOrEmpty(Chronicle.CurrentAgeId) || Chronicle.MemoryCount > 0; detail = $"ageId='{Chronicle.CurrentAgeId}' ageName='{Chronicle.CurrentAgeName}' memory={Chronicle.MemoryCount}"; break; } case "world_memory_world_only": { // World tab must not mix in character Kill/Death/Lover rows. IReadOnlyList snap = Chronicle.SnapshotMemory(); int bad = 0; int worldRows = 0; for (int i = 0; i < snap.Count; i++) { ChronicleEntry e = snap[i]; if (e == null || e.Kind == ChronicleKind.AgeChapter) { continue; } if (e.Kind == ChronicleKind.World) { worldRows++; continue; } bad++; } pass = bad == 0; detail = $"badKinds={bad} worldRows={worldRows} memory={snap.Count}"; break; } case "chronicle_subjects_capped": { int subjects = Chronicle.HistorySubjectCount; int dossiers = Chronicle.FinalDossierCount; pass = subjects <= Chronicle.MaxHistorySubjects && dossiers <= Chronicle.MaxHistorySubjects; detail = $"subjects={subjects} dossiers={dossiers} max={Chronicle.MaxHistorySubjects} revision={Chronicle.HistoryRevision}"; break; } case "chronicle_memory_capped": { int memory = Chronicle.MemoryCount; pass = memory <= Chronicle.MaxLandmarks; detail = $"memory={memory} max={Chronicle.MaxLandmarks}"; break; } case "dossier_favorite": case "is_favorite": { Actor unit = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; bool fav = false; try { fav = unit != null && unit.isAlive() && unit.isFavorite(); } catch { // ignore } if (!fav) { long id = WatchCaption.CurrentUnitId; if (id == 0) { id = ChronicleHud.DetailUnitId; } if (id == 0 && unit != null) { try { id = unit.getID(); } catch { id = 0; } } fav = Chronicle.IsFavoriteSubject(id); } string wantRaw = (cmd.value ?? "true").Trim().ToLowerInvariant(); bool want = wantRaw != "false" && wantRaw != "0" && wantRaw != "off"; pass = fav == want; detail = $"favorite={fav} want={want}"; break; } case "dossier_history_shown": { int want = ParseCountExpect(cmd, defaultValue: 1); string scope = (cmd.tier ?? "").Trim().ToLowerInvariant(); int have = scope == "life" ? WatchCaption.LastLifeShown : scope == "activity" ? WatchCaption.LastActivityShown : WatchCaption.LastHistoryShown; string cmp = (cmd.label ?? "min").Trim().ToLowerInvariant(); if (cmp == "exact" || cmp == "==") { pass = have == want; } else { pass = have >= want; } detail = $"shown={have} want={want} scope={scope} total={WatchCaption.LastHistoryShown} act={WatchCaption.LastActivityShown} life={WatchCaption.LastLifeShown} scrollable={WatchCaption.LastHistoryScrollable} histFill={WatchCaption.LastHistoryFillsBody}"; break; } case "dossier_history_scrollable": { // Dossier history is no longer scrollable; full history lives in Lore. pass = !WatchCaption.LastHistoryScrollable && WatchCaption.LastHistoryShown <= 3; detail = $"scrollable={WatchCaption.LastHistoryScrollable} shown={WatchCaption.LastHistoryShown}"; break; } case "lore_detail": { bool want = ParseBool(cmd.value, defaultValue: true); bool have = ChronicleHud.DetailUnitId != 0; // want=true: Lore open with a subject; want=false: no detail (panel may be closed). pass = want ? (Chronicle.HudVisible && have) : !have; detail = $"detailId={ChronicleHud.DetailUnitId} want={want} visible={Chronicle.HudVisible} tab={ChronicleHud.ActiveTab} follow={ChronicleHud.FollowFocus}"; break; } case "lore_follow": { bool want = ParseBool(cmd.value, defaultValue: true); bool have = ChronicleHud.FollowFocus; pass = Chronicle.HudVisible && ChronicleHud.DetailUnitId != 0 && have == want; detail = $"follow={have} want={want} detailId={ChronicleHud.DetailUnitId}"; break; } case "fallen_focus": { string want = (cmd.value ?? "").Trim().ToLowerInvariant(); string have = (Chronicle.LastFallenFocusDetail ?? "").Trim().ToLowerInvariant(); if (want == "killer" || want.StartsWith("killer")) { pass = have.StartsWith("killer:"); } else if (want == "place") { pass = have == "place"; } else if (want == "none") { pass = have == "none" || string.IsNullOrEmpty(have); } else { pass = have == want; } detail = $"fallenFocus={Chronicle.LastFallenFocusDetail} want={cmd.value}"; break; } case "grave_stacks": { // Prefer value when set so want=0 works (count>0 would skip an explicit 0). int want = !string.IsNullOrEmpty(cmd.value) ? ParseInt(cmd.value, 0) : cmd.count; int have = GraveMarkers.StackCount; string cmp = (cmd.label ?? "").Trim().ToLowerInvariant(); if (cmp == "min" || cmp == ">=" || string.IsNullOrEmpty(cmp)) { pass = have >= want; } else if (cmp == "max" || cmp == "<=") { pass = have <= want; } else if (cmp == "eq" || cmp == "==") { pass = have == want; } else { pass = have >= want; } detail = $"stacks={have} want={want} cmp={cmp}"; break; } case "grave_entries": { int want = cmd.count > 0 ? cmd.count : ParseInt(cmd.value, 1); Vector3 pos = Vector3.zero; if (MoveCamera.instance != null) { pos = MoveCamera.instance.transform.position; pos.z = 0f; } if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null) { pos = MoveCamera._focus_unit.current_position; } int tx = Mathf.FloorToInt(pos.x); int ty = Mathf.FloorToInt(pos.y); int have = GraveMarkers.EntryCountAt(tx, ty); pass = have >= want; detail = $"tile={tx},{ty} entries={have} want={want}"; break; } case "grave_stack_scope": case "grave_panel": { // grave_panel kept as alias during transition; means Lore grave-stack scope. bool want = ParseBool(cmd.value, defaultValue: true); bool have = Chronicle.HudVisible && ChronicleHud.GraveStackScopeActive; pass = have == want; detail = $"scope={ChronicleHud.GraveStackScopeActive} lore={Chronicle.HudVisible} want={want} detail={ChronicleHud.DetailUnitId} filtered={ChronicleHud.GraveStackFilteredCount}"; break; } case "grave_permanent": { bool want = ParseBool(cmd.value, defaultValue: true); int tx = ChronicleHud.GraveStackTileX; int ty = ChronicleHud.GraveStackTileY; if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null) { Vector3 p = MoveCamera._focus_unit.current_position; int ftx = Mathf.FloorToInt(p.x); int fty = Mathf.FloorToInt(p.y); if (GraveMarkers.TryGetStack(ftx, fty, out _)) { tx = ftx; ty = fty; } } bool have = GraveMarkers.IsStackPermanent(tx, ty); pass = have == want; detail = $"tile={tx},{ty} permanent={have} want={want}"; break; } case "grave_search_filtered": { int want = cmd.count > 0 ? cmd.count : ParseInt(cmd.value, 1); pass = ChronicleHud.GraveStackScopeActive && ChronicleHud.DetailUnitId == 0 && ChronicleHud.GraveStackFilteredCount == want; detail = $"filtered={ChronicleHud.GraveStackFilteredCount} want={want} query='{ChronicleHud.SearchText}' scope={ChronicleHud.GraveStackScopeActive}"; break; } case "grave_search_layout": { bool fills = ChronicleHud.GraveSearchFillsTools; pass = ChronicleHud.GraveStackScopeActive && ChronicleHud.DetailUnitId == 0 && fills; detail = $"fills={fills} scope={ChronicleHud.GraveStackScopeActive} detail={ChronicleHud.DetailUnitId}"; break; } case "grave_death_subtitle": { string needle = (cmd.value ?? "").Trim(); long id = ChronicleHud.DetailUnitId; string have = id != 0 ? Chronicle.FormatGraveDeathSubtitle(id) : ""; pass = !string.IsNullOrEmpty(needle) && !string.IsNullOrEmpty(have) && have.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0; detail = $"subtitle='{have}' wantContains='{needle}' detailId={id}"; break; } case "death_manner": { long id = ChronicleHud.DetailUnitId; DeathManner have = Chronicle.LatestDeathMannerFor(id); string haveLabel = Chronicle.DeathMannerLabel(have); string want = (cmd.value ?? "").Trim(); pass = id != 0 && (haveLabel.Equals(want, System.StringComparison.OrdinalIgnoreCase) || have.ToString().Equals(want, System.StringComparison.OrdinalIgnoreCase)); detail = $"manner={have} label={haveLabel} want={cmd.value} detailId={id}"; break; } case "lore_follows_focus": { long focusId = WatchCaption.CurrentUnitId; if (focusId == 0) { focusId = Chronicle.CurrentHistorySubjectId(); } long loreId = ChronicleHud.DetailUnitId; pass = Chronicle.HudVisible && ChronicleHud.FollowFocus && SpectatorMode.Active && focusId != 0 && loreId == focusId; detail = $"follow={ChronicleHud.FollowFocus} idle={SpectatorMode.Active} focusId={focusId} loreId={loreId}"; break; } case "lore_history_shown": { int want = ParseCountExpect(cmd, defaultValue: 1); long detailId = ChronicleHud.DetailUnitId; int haveData = Chronicle.HistoryCountFor(detailId); int haveUi = ChronicleHud.LastDetailHistoryRows; string cmp = (cmd.label ?? "min").Trim().ToLowerInvariant(); bool dataOk = cmp == "exact" || cmp == "==" ? haveData == want : haveData >= want; bool uiOk = cmp == "exact" || cmp == "==" ? haveUi == want : haveUi >= want; pass = Chronicle.HudVisible && detailId != 0 && dataOk && uiOk; detail = $"loreHist={haveData} uiRows={haveUi} want={want} detailId={detailId} dossierId={WatchCaption.CurrentUnitId}"; break; } case "dossier_lore_history_match": { long dossierId = WatchCaption.CurrentUnitId; long loreId = ChronicleHud.DetailUnitId; int lifeShown = WatchCaption.LastLifeShown; int loreData = Chronicle.HistoryCountFor(loreId); int loreUi = ChronicleHud.LastDetailHistoryRows; bool idsMatch = dossierId != 0 && dossierId == loreId; // Life lines must agree with Lore; activity-only peek does not imply chronicle rows. bool countsAgree = lifeShown <= 0 ? loreData == 0 && loreUi == 0 : loreData >= lifeShown && loreUi >= Mathf.Min(lifeShown, 24); pass = Chronicle.HudVisible && idsMatch && countsAgree; detail = $"dossierId={dossierId} loreId={loreId} lifeShown={lifeShown} act={WatchCaption.LastActivityShown} loreData={loreData} loreUi={loreUi} preview={WatchCaption.LastHistoryPreview}"; break; } case "dossier_pinned": { bool want = ParseBool(cmd.value, defaultValue: true); bool have = WatchCaption.PinnedWhilePaused && WatchCaption.Visible; pass = have == want; detail = $"pinned={WatchCaption.PinnedWhilePaused} visible={WatchCaption.Visible} idle={SpectatorMode.Active} want={want}"; break; } case "dossier_unit_matches_lore": { long dossierId = WatchCaption.CurrentUnitId; long loreId = ChronicleHud.DetailUnitId; pass = dossierId != 0 && loreId != 0 && dossierId == loreId; detail = $"dossierId={dossierId} loreId={loreId} headline={WatchCaption.LastHeadline}"; break; } case "dossier_species": { string want = (cmd.value ?? cmd.asset ?? "").Trim(); string have = WatchCaption.Current != null ? WatchCaption.Current.SpeciesId ?? "" : ""; pass = !string.IsNullOrEmpty(want) && have.Equals(want, System.StringComparison.OrdinalIgnoreCase); detail = $"species={have} want={want} headline={WatchCaption.LastHeadline} avatarLive={DossierAvatar.ShowingLiveAvatar} avatarSpecies={DossierAvatar.ShowingSpeciesFallback}"; break; } case "dossier_matches_focus": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; UnitDossier dossier = WatchCaption.Current; string focusSpecies = focus?.asset != null ? focus.asset.id : ""; long focusId = 0; try { focusId = focus != null ? focus.getID() : 0; } catch { focusId = 0; } pass = focus != null && focus.isAlive() && dossier != null && dossier.UnitId == focusId && !string.IsNullOrEmpty(focusSpecies) && focusSpecies.Equals(dossier.SpeciesId ?? "", System.StringComparison.OrdinalIgnoreCase) && DossierAvatar.ShowingLiveAvatar && !DossierAvatar.ShowingSpeciesFallback; detail = $"focusId={focusId} dossierId={(dossier != null ? dossier.UnitId : 0)} focusSpecies={focusSpecies} dossierSpecies={(dossier != null ? dossier.SpeciesId : "")} avatarLive={DossierAvatar.ShowingLiveAvatar} avatarSpecies={DossierAvatar.ShowingSpeciesFallback}"; break; } case "dossier_avatar_live": { pass = DossierAvatar.ShowingLiveAvatar && !DossierAvatar.ShowingSpeciesFallback; detail = $"avatarLive={DossierAvatar.ShowingLiveAvatar} avatarSpecies={DossierAvatar.ShowingSpeciesFallback} dossierId={WatchCaption.CurrentUnitId}"; break; } case "dossier_avatar_species": { pass = DossierAvatar.ShowingSpeciesFallback && !DossierAvatar.ShowingLiveAvatar; detail = $"avatarLive={DossierAvatar.ShowingLiveAvatar} avatarSpecies={DossierAvatar.ShowingSpeciesFallback} dossierId={WatchCaption.CurrentUnitId}"; break; } case "fallen_place_no_follow": { // Place focus must clear unit follow so the camera stays at the death site. pass = Chronicle.LastFallenFocusDetail == "place" && !MoveCamera.hasFocusUnit(); detail = $"fallenFocus={Chronicle.LastFallenFocusDetail} hasFocus={MoveCamera.hasFocusUnit()} pos={Chronicle.LastFallenFocusPosition}"; break; } case "world_jump_place": { pass = Chronicle.LastJumpDetail == "place" && !MoveCamera.hasFocusUnit() && Chronicle.LastJumpPosition.sqrMagnitude > 0.0001f; detail = $"jump={Chronicle.LastJumpDetail} hasFocus={MoveCamera.hasFocusUnit()} pos={Chronicle.LastJumpPosition}"; break; } case "fallen_killer_camera": { Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; long focusId = 0; try { focusId = focus != null ? focus.getID() : 0; } catch { focusId = 0; } string detailStr = Chronicle.LastFallenFocusDetail ?? ""; bool killerTag = detailStr.StartsWith("killer:"); long killerId = 0; if (killerTag) { long.TryParse(detailStr.Substring("killer:".Length), out killerId); } pass = killerTag && killerId != 0 && focusId == killerId && WatchCaption.CurrentUnitId == focusId && DossierAvatar.ShowingLiveAvatar; detail = $"fallenFocus={detailStr} focusId={focusId} dossierId={WatchCaption.CurrentUnitId} avatarLive={DossierAvatar.ShowingLiveAvatar}"; break; } case "lore_zoom_blocked": { MoveCamera cam = MoveCamera.instance; if (cam == null) { pass = false; detail = "no camera"; break; } float before = cam.getTargetZoom(); CameraZoomPatches.ForceAbsorbForHarness = true; try { MoveCamera.zoomInWheel(null); MoveCamera.zoomOutWheel(null); } finally { CameraZoomPatches.ForceAbsorbForHarness = false; } float after = cam.getTargetZoom(); pass = Mathf.Abs(after - before) < 0.001f; detail = $"zoomBefore={before} zoomAfter={after} absorb={ChronicleHud.ShouldAbsorbScrollZoom()}"; break; } case "dossier_history_fills_body": { pass = WatchCaption.LastHistoryShown > 0 && WatchCaption.LastHistoryFillsBody; detail = $"histFill={WatchCaption.LastHistoryFillsBody} shown={WatchCaption.LastHistoryShown} size={WatchCaption.LastPanelSize}"; break; } case "hud_no_overlap": { bool hasDossier = WatchCaption.TryGetScreenPixelRect(out Rect dossier); bool hasMemory = ChronicleHud.TryGetScreenPixelRect(out Rect memory); bool overlap = false; if (hasDossier && hasMemory && ChronicleHud.Visible) { overlap = dossier.Overlaps(memory); } pass = hasDossier && (!ChronicleHud.Visible || (hasMemory && !overlap)); detail = $"dossier={(hasDossier ? dossier.ToString() : "-")} memory={(hasMemory ? memory.ToString() : "-")} visible={ChronicleHud.Visible} overlap={overlap}"; break; } case "world_memory_compact": { bool hasMemory = ChronicleHud.TryGetScreenPixelRect(out Rect memory); // Same canvas scale as dossier (no localScale damp). Reject tiny or near-fullscreen. float minW = Screen.width * 0.22f; float maxW = Screen.width * 0.55f; float minH = Screen.height * 0.16f; float maxH = Screen.height * 0.50f; pass = ChronicleHud.Visible && hasMemory && memory.width >= minW && memory.height >= minH && memory.width <= maxW && memory.height <= maxH; detail = $"memory={(hasMemory ? memory.ToString() : "-")} screen={Screen.width}x{Screen.height} band=({minW:0}-{maxW:0} x {minH:0}-{maxH:0})"; break; } case "chronicle_tab": case "lore_tab": { string wantRaw = (cmd.value ?? cmd.label ?? "world").Trim().ToLowerInvariant(); ChronicleHud.LoreTab want = ChronicleHud.LoreTab.World; if (wantRaw == "fallen" || wantRaw == "dead") { want = ChronicleHud.LoreTab.Fallen; } else if (wantRaw == "living" || wantRaw == "characters" || wantRaw == "character" || wantRaw == "chars" || wantRaw == "history") { want = ChronicleHud.LoreTab.Living; } pass = Chronicle.HudVisible && ChronicleHud.ActiveTab == want; detail = $"hud={Chronicle.HudVisible} tab={ChronicleHud.ActiveTab} want={want} recent={Chronicle.RecentFocusCount}"; break; } case "fallen_list_top": { string want = (cmd.value ?? cmd.label ?? "").Trim(); string have = ChronicleHud.LastListTopTitle ?? ""; pass = Chronicle.HudVisible && ChronicleHud.ActiveTab == ChronicleHud.LoreTab.Fallen && ChronicleHud.DetailUnitId == 0 && !string.IsNullOrEmpty(want) && have.IndexOf(want, System.StringComparison.OrdinalIgnoreCase) >= 0; detail = $"top='{have}' want='{want}' tab={ChronicleHud.ActiveTab}"; break; } case "fallen_list_top_not": { string reject = (cmd.value ?? cmd.label ?? "").Trim(); string have = ChronicleHud.LastListTopTitle ?? ""; pass = Chronicle.HudVisible && ChronicleHud.ActiveTab == ChronicleHud.LoreTab.Fallen && ChronicleHud.DetailUnitId == 0 && !string.IsNullOrEmpty(reject) && have.IndexOf(reject, System.StringComparison.OrdinalIgnoreCase) < 0; detail = $"top='{have}' reject='{reject}' tab={ChronicleHud.ActiveTab}"; break; } case "fallen_list_before": { string newer = (cmd.value ?? "").Trim(); string older = (cmd.label ?? "").Trim(); List rows = Chronicle.ListCharacters( max: 0, scope: Chronicle.CharacterListScope.Fallen); int newerIndex = -1; int olderIndex = -1; for (int i = 0; i < rows.Count; i++) { string title = rows[i]?.Title ?? ""; if (newerIndex < 0 && title.IndexOf(newer, System.StringComparison.OrdinalIgnoreCase) >= 0) { newerIndex = i; } if (olderIndex < 0 && title.IndexOf(older, System.StringComparison.OrdinalIgnoreCase) >= 0) { olderIndex = i; } } pass = Chronicle.HudVisible && ChronicleHud.ActiveTab == ChronicleHud.LoreTab.Fallen && !string.IsNullOrEmpty(newer) && !string.IsNullOrEmpty(older) && newerIndex >= 0 && olderIndex >= 0 && newerIndex < olderIndex; detail = $"newer='{newer}' newerIndex={newerIndex} older='{older}' olderIndex={olderIndex} rows={rows.Count}"; break; } case "lore_list_stale": { bool want = ParseBool(cmd.value, defaultValue: true); if (string.IsNullOrEmpty(cmd.value) && !string.IsNullOrEmpty(cmd.expect) && cmd.expect != "lore_list_stale") { want = ParseBool(cmd.expect, defaultValue: true); } bool have = ChronicleHud.CastListStale; bool castTab = ChronicleHud.ActiveTab == ChronicleHud.LoreTab.Living || ChronicleHud.ActiveTab == ChronicleHud.LoreTab.Fallen; pass = Chronicle.HudVisible && castTab && ChronicleHud.DetailUnitId == 0 && have == want; detail = $"stale={have} want={want} tab={ChronicleHud.ActiveTab} revision={Chronicle.HistoryRevision}"; break; } case "lore_recent": { int want = ParseCountExpect(cmd, defaultValue: 1); int have = Chronicle.RecentFocusCount; pass = have >= want; detail = $"recent={have} min={want}"; break; } default: pass = false; detail = "unknown expect: " + expect; break; } if (pass) { _assertPass++; _cmdOk++; } else { _assertFail++; _cmdFail++; } Emit(cmd, pass, detail); LogService.LogInfo( $"[IdleSpectator][ASSERT] {(pass ? "PASS" : "FAIL")} id={cmd.id} expect={expect} {detail}"); } private static bool WantsPreserveSaga(HarnessCommand cmd) { string v = (cmd?.value ?? cmd?.expect ?? "").Trim().ToLowerInvariant(); return v == "preserve" || v == "soak" || v == "keep" || v == "keep_saga" || v == "preserve_saga"; } private static void DoSnapshot(HarnessCommand cmd) { string drops = InterestDropLog.FormatRecent(6); string snap = $"idle={SpectatorMode.Active} focus={MoveCamera.hasFocusUnit()} powerBar={CanvasMain.isBottomBarShowing()} unit={FocusLabel()} tip={CameraDirector.LastWatchLabel} tipAsset={CameraDirector.LastWatchAssetId} bad={StateProbe.BadEventCount} score={InterestDirector.CurrentScoreLabel} discoveryPending={SpeciesDiscovery.PendingCount} caption={WatchCaption.LastHeadline} chronicle={Chronicle.Count} drops={InterestDropLog.Count}"; if (!string.IsNullOrEmpty(drops)) { snap += " recentDrop=[" + drops + "]"; } _cmdOk++; Emit(cmd, ok: true, detail: snap); LogService.LogInfo("[IdleSpectator][SNAP] " + snap); } /// /// Read-only AFK dump: roster vs camera, Active/Involved chrome, rival/crossover counts. /// Preserves saga roster+memory when the batch ends. /// private static void DoSoakProbe(HarnessCommand cmd) { _preserveSagaAfterBatch = true; float now = Time.unscaledTime; LifeSagaRoster.Tick(now); LifeSagaRail.Refresh(); Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; long focusId = EventFeedUtil.SafeId(focus); InterestCandidate tip = InterestDirector.CurrentCandidate; bool focusMc = focusId != 0 && LifeSagaRoster.IsMc(focusId); bool focusPrefer = focusId != 0 && LifeSagaRoster.IsPrefer(focusId); bool focusCast = focusId != 0 && LifeSagaRoster.IsMcCast(focusId); int softRank = tip != null ? LifeSagaRoster.SoftBiasRank(tip) : 0; int touchedMcs = tip != null ? LifeSagaRoster.CountTouchedMcs(tip) : 0; bool rival = false; string rivalNote = ""; if (LifeSagaMemory.TryGetEarnedRival(focusId, out LifeSagaFact rivalFact) && rivalFact != null) { rival = true; rivalNote = !string.IsNullOrEmpty(rivalFact.Note) ? rivalFact.Note : (rivalFact.Provenance ?? rivalFact.Kind.ToString()); } int facts = 0; int crossovers = 0; if (focusId != 0) { var list = LifeSagaMemory.FactsFor(focusId); facts = list != null ? list.Count : 0; if (list != null) { for (int i = 0; i < list.Count; i++) { if (list[i] != null && list[i].McCrossover) { crossovers++; } } } } var board = new List(LifeSagaRoster.Cap); LifeSagaRoster.CopySlots(board); var names = new StringBuilder(320); for (int i = 0; i < board.Count; i++) { LifeSagaSlot s = board[i]; if (s == null || s.UnitId == 0) { continue; } if (names.Length > 0) { names.Append(','); } string n = string.IsNullOrEmpty(s.DisplayName) ? ("#" + s.UnitId) : s.DisplayName; if (s.UnitId == focusId) { n = "*" + n; } if (s.Prefer || s.GameFavorite) { n += "^"; } names.Append(n) .Append("{reason=") .Append(s.AdmissionReason) .Append(" pop=") .Append(s.SpeciesPopulation) .Append(" rare=") .Append(s.IsRareCreature) .Append(" special=") .Append(s.IsSpecialCreature) .Append(" antagonist=") .Append(s.IsRecurringAntagonist) .Append(" anchor=") .Append(s.AntagonistAnchorMcId) .Append(" evidence=") .Append(s.AntagonistEvidence) .Append(" score=") .Append(LifeSagaRoster.EffectiveInterest(s, now).ToString("0.##")) .Append('}'); } string tipLabel = tip?.Label ?? CameraDirector.LastWatchLabel ?? ""; string detail = $"focus={SafeName(focus)} id={focusId} onRoster={focusMc} prefer={focusPrefer} cast={focusCast} " + $"activeLit={LifeSagaRail.LastActiveHighlight} involvedLit={LifeSagaRail.LastInvolvedHighlightCount} " + $"softRank={softRank} touchedMcs={touchedMcs} " + $"rival={rival} crossoverFacts={crossovers} facts={facts} lives={LifeSagaMemory.LifeCount} " + $"roster={LifeSagaRoster.Count} tip={tipLabel} " + $"score={InterestDirector.CurrentScoreLabel} names=[{names}] " + $"lastAdmission=[{LifeSagaRoster.LastAdmissionDiagnostic}]"; _cmdOk++; Emit(cmd, ok: true, detail: detail); LogService.LogInfo("[IdleSpectator][SOAK] " + detail); try { EnsureDirs(); string path = Path.Combine(HarnessDir(), "soak-probe.txt"); File.WriteAllText( path, detail + Environment.NewLine + "rivalNote=" + rivalNote + Environment.NewLine + "preserved=true" + Environment.NewLine); } catch { // ignore IO } } private static void DoDiscoveryNote(HarnessCommand cmd) { string assetId = ResolveAssetId(cmd); if (string.IsNullOrEmpty(assetId)) { _cmdFail++; Emit(cmd, ok: false, detail: "missing asset for discovery_note"); return; } Vector3 pos = CameraPos(); Actor unit = ResolveUnit(assetId); if (unit != null) { pos = unit.current_position; } bool ok = SpeciesDiscovery.HarnessNoteAsset(assetId, pos); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: $"asset={assetId} pending={SpeciesDiscovery.PendingCount}"); } private static void DoWatchGhost(HarnessCommand cmd) { if (!WorldReady()) { if (RetryFront(cmd, "world_not_ready")) { return; } _cmdFail++; Emit(cmd, ok: false, detail: "world_not_ready"); return; } Actor near = ResolveUnit(_lastSpawnedAssetId); if (near == null) { near = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 5000f); } if (near == null) { _cmdFail++; Emit(cmd, ok: false, detail: "no unit near camera for ghost watch"); return; } // Claim a species that is extremely unlikely to match the focused unit. string fakeAsset = string.IsNullOrEmpty(cmd.asset) || cmd.asset == "auto" ? "__ghost_species__" : cmd.asset; string label = string.IsNullOrEmpty(cmd.label) ? $"New species: {fakeAsset}" : cmd.label.Replace("{asset}", fakeAsset); InterestEvent interest = new InterestEvent { Score = 50f, Position = near.current_position, FollowUnit = null, Label = label, CreatedAt = Time.unscaledTime, AssetId = fakeAsset }; string before = FocusKey(); CameraDirector.Watch(interest); string after = FocusKey(); bool unchanged = before == after && MoveCamera.hasFocusUnit(); if (unchanged) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, unchanged, detail: $"ghost={fakeAsset} before={before} after={after} tip={CameraDirector.LastWatchLabel}"); } private static string ResolveAssetId(HarnessCommand cmd) { string assetId = string.IsNullOrEmpty(cmd.asset) ? cmd.value : cmd.asset; if (assetId == "auto" || string.IsNullOrEmpty(assetId)) { assetId = _lastSpawnedAssetId; } return assetId ?? ""; } private static string FocusKey() { if (!MoveCamera.hasFocusUnit() || MoveCamera._focus_unit == null) { return "-"; } Actor unit = MoveCamera._focus_unit; string asset = unit.asset != null ? unit.asset.id : "?"; return $"{asset}@{unit.GetHashCode()}"; } private static void IngestNewCommands() { string path = CommandsPath(); if (!File.Exists(path)) { return; } long length; try { length = new FileInfo(path).Length; } catch { return; } if (length < _offset) { _offset = 0; } if (length == _offset) { return; } try { using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { fs.Seek(_offset, SeekOrigin.Begin); using (StreamReader reader = new StreamReader(fs, Encoding.UTF8)) { string line; while ((line = reader.ReadLine()) != null) { _offset = fs.Position; line = line.Trim(); if (string.IsNullOrEmpty(line) || line.StartsWith("#")) { continue; } HarnessCommand cmd = null; try { cmd = JsonUtility.FromJson(line); } catch { LogService.LogInfo("[IdleSpectator][HARNESS] bad json: " + line); continue; } if (cmd == null || string.IsNullOrEmpty(cmd.action)) { continue; } if (string.IsNullOrEmpty(cmd.id)) { cmd.id = Guid.NewGuid().ToString("N").Substring(0, 8); } Queue.Enqueue(cmd); } } } File.WriteAllText(OffsetPath(), _offset.ToString(CultureInfo.InvariantCulture)); } catch (Exception ex) { LogService.LogInfo("[IdleSpectator][HARNESS] ingest error: " + ex.Message); } } private static void Emit(HarnessCommand cmd, bool ok, string detail) { string line = $"{{\"id\":\"{Escape(cmd.id)}\",\"action\":\"{Escape(cmd.action)}\",\"ok\":{(ok ? "true" : "false")},\"detail\":\"{Escape(detail)}\"}}"; try { File.AppendAllText(ResultsPath(), line + "\n"); } catch { // ignore } LogService.LogInfo( $"[IdleSpectator][HARNESS] {(ok ? "ok" : "FAIL")} id={cmd.id} action={cmd.action} detail={detail}"); } private static void WriteLastResult(string status, string detail) { LastResult result = new LastResult { batch_id = _lastBatchId, status = status, ok = _cmdOk, fail = _cmdFail, assert_pass = _assertPass, assert_fail = _assertFail, idle = SpectatorMode.Active.ToString(), focus = MoveCamera.hasFocusUnit().ToString(), power_bar = CanvasMain.isBottomBarShowing().ToString(), unit = FocusLabel(), tip = CameraDirector.LastWatchLabel ?? "", detail = detail ?? "", updated_at = DateTime.UtcNow.ToString("o") }; try { File.WriteAllText(LastResultPath(), JsonUtility.ToJson(result, true)); } catch { // ignore } } private static bool WorldReady() { return Config.game_loaded && World.world != null && !SmoothLoader.isLoading(); } private static bool HasAnyAliveUnit() { return WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 5000f) != null; } private static bool ShouldAutoDismissUi() { if (Queue.Count > 0 || _waitUntil > 0f) { return true; } string dir = HarnessDir(); if (string.IsNullOrEmpty(dir)) { return false; } if (File.Exists(Path.Combine(dir, "auto-dismiss"))) { return true; } string commands = CommandsPath(); if (!File.Exists(commands)) { return false; } try { return new FileInfo(commands).Length > _offset; } catch { return false; } } /// /// Closes Welcome Back (and optionally any ScrollWindow) so map gen / play can proceed. /// public static bool TryDismissBlockingWindows(bool forceAll = false) { if (!ScrollWindow.isWindowActive()) { return false; } ScrollWindow current = ScrollWindow.getCurrentWindow(); string id = current != null ? current.screen_id : ""; bool welcome = id == "welcome" || (current != null && current.GetComponent() != null); if (!welcome && !forceAll) { return false; } ScrollWindow.hideAllEvent(pWithAnimation: false); LogService.LogInfo($"[IdleSpectator][HARNESS] dismissed window id={id}"); return true; } private static bool IsBlockingWindowOpen() { if (!ScrollWindow.isWindowActive()) { return false; } ScrollWindow current = ScrollWindow.getCurrentWindow(); if (current == null) { return false; } if (current.screen_id == "welcome") { return true; } return current.GetComponent() != null; } private static WorldTile TileNearCamera() { Vector3 pos = CameraPos(); int x = Mathf.RoundToInt(pos.x); int y = Mathf.RoundToInt(pos.y); WorldTile tile = World.world.GetTile(x, y); if (tile != null) { return tile; } return World.world.GetTileSimple(x, y); } private static Vector3 CameraPos() { if (MoveCamera.instance != null) { return MoveCamera.instance.transform.position; } return Vector3.zero; } private static string FocusLabel() { if (!MoveCamera.hasFocusUnit() || MoveCamera._focus_unit == null) { return "-"; } return SafeName(MoveCamera._focus_unit); } private static bool IsEnsembleCombatTip(string tip) { if (string.IsNullOrEmpty(tip)) { return false; } return tip.StartsWith("Duel", StringComparison.OrdinalIgnoreCase) || tip.StartsWith("Skirmish", StringComparison.OrdinalIgnoreCase) || tip.StartsWith("Battle", StringComparison.OrdinalIgnoreCase) || tip.StartsWith("Mass", StringComparison.OrdinalIgnoreCase) || tip.StartsWith("War -", StringComparison.OrdinalIgnoreCase) || tip.StartsWith("Plot -", StringComparison.OrdinalIgnoreCase) || tip.StartsWith("Pack -", StringComparison.OrdinalIgnoreCase) || tip.StartsWith("Outbreak -", StringComparison.OrdinalIgnoreCase) || tip.IndexOf(" vs ", StringComparison.OrdinalIgnoreCase) >= 0; } private static long ResolveActivitySubjectId() { // Prefer the camera focus (harness apply/assert target) over watch caption, // which can still point at a prior spectator subject during nested regression. try { if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null) { long focusId = MoveCamera._focus_unit.getID(); if (focusId != 0) { _activitySubjectId = focusId; return focusId; } } } catch { // ignore } if (_activitySubjectId != 0) { return _activitySubjectId; } return WatchCaption.CurrentUnitId; } private static string SafeName(Actor actor) { if (actor == null) { return "-"; } try { string name = actor.getName(); if (!string.IsNullOrEmpty(name)) { return name; } } catch { // ignore } return actor.asset != null ? actor.asset.id : "unit"; } /// /// If the reason line names a living unit that is not the scene subject/related, return that name. /// private static string FindStrangerNamedInReason(string reason, InterestCandidate scene) { if (string.IsNullOrEmpty(reason) || scene == null || World.world?.units == null) { return ""; } string subjectName = EventFeedUtil.SafeName(scene.FollowUnit); string relatedName = EventFeedUtil.SafeName(scene.RelatedUnit); try { foreach (Actor unit in World.world.units) { if (unit == null || !unit.isAlive()) { continue; } if (unit == scene.FollowUnit || unit == scene.RelatedUnit) { continue; } string name = SafeName(unit); if (string.IsNullOrEmpty(name) || name.Length < 3) { continue; } if (!string.IsNullOrEmpty(subjectName) && name.Equals(subjectName, StringComparison.OrdinalIgnoreCase)) { continue; } if (!string.IsNullOrEmpty(relatedName) && name.Equals(relatedName, StringComparison.OrdinalIgnoreCase)) { continue; } if (reason.IndexOf(name, StringComparison.OrdinalIgnoreCase) >= 0) { return name; } } } catch { // ignore } return ""; } private static float EventStrengthFromTierHint(string raw, float fallback) { if (string.IsNullOrEmpty(raw)) { return fallback; } switch (raw.Trim().ToLowerInvariant()) { case "ambient": return 25f; case "curiosity": return 40f; case "action": // Clears cutInMargin (35) over enriched Curiosity CharacterLed (~64). return 100f; case "story": return 80f; case "epic": // Remain decisive after same-subject/arc variety penalties and the sticky cut margin. return 250f; default: return fallback; } } private static InterestLeadKind LeadFromTierHint(string raw, InterestLeadKind fallback) { if (string.IsNullOrEmpty(raw)) { return fallback; } string s = raw.Trim().ToLowerInvariant(); if (s == "ambient" || s == "curiosity" || s == "story") { return InterestLeadKind.CharacterLed; } if (s == "action" || s == "epic") { return InterestLeadKind.EventLed; } return fallback; } private static int ParseCountExpect(HarnessCommand cmd, int defaultValue) { // Prefer explicit value= so Step()'s default count=1 does not override want=0. if (!string.IsNullOrEmpty(cmd.value) && int.TryParse(cmd.value.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out int fromValue)) { return fromValue; } if (cmd.count != 1 || string.IsNullOrEmpty(cmd.value)) { // count explicitly set via Step(..., count: N) when N != default 1, or no value given. if (string.IsNullOrEmpty(cmd.value)) { return cmd.count; } } return defaultValue; } private static string SampleRailTip() { LifeSagaRoster.Tick(Time.unscaledTime); LifeSagaRail.Refresh(); string have = LifeSagaRail.LastTipSample ?? ""; if (!string.IsNullOrEmpty(have)) { return have; } var slots = new System.Collections.Generic.List(10); LifeSagaRoster.CopySlots(slots); if (slots.Count > 0 && slots[0] != null) { return LifeSagaPresentation.BuildPlainText(slots[0]) ?? ""; } return ""; } private static bool ParseBool(string raw, bool defaultValue) { if (string.IsNullOrEmpty(raw)) { return defaultValue; } raw = raw.Trim().ToLowerInvariant(); if (raw == "1" || raw == "true" || raw == "on" || raw == "yes") { return true; } if (raw == "0" || raw == "false" || raw == "off" || raw == "no") { return false; } return defaultValue; } private static float ParseFloat(string raw, float defaultValue) { if (float.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out float v)) { return v; } return defaultValue; } private static int ParseInt(string raw, int defaultValue) { if (int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out int v)) { return v; } return defaultValue; } /// /// Load a numbered save slot (1-5). Hitch soaks use slot 2 (Box of Magic). /// private static bool TryLoadSaveSlot(int slot, out string detail) { detail = ""; try { Type smType = AccessTools.TypeByName("SaveManager"); if (smType == null) { detail = "SaveManager type missing"; return false; } var setSlot = AccessTools.Method(smType, "setCurrentSlot", new[] { typeof(int) }); var getPath = AccessTools.Method(smType, "getSlotSavePath", new[] { typeof(int) }); if (setSlot == null) { detail = "setCurrentSlot missing"; return false; } setSlot.Invoke(null, new object[] { slot }); string path = getPath?.Invoke(null, new object[] { slot }) as string ?? ""; object instance = null; object map = World.world; if (map == null) { var instFi = AccessTools.Field(typeof(MapBox), "instance"); map = instFi?.GetValue(null); } if (map != null) { var smFi = AccessTools.Field(map.GetType(), "save_manager") ?? AccessTools.Field(typeof(MapBox), "save_manager"); instance = smFi?.GetValue(map); } if (instance == null) { detail = $"save_manager missing after setCurrentSlot={slot} path={path}"; return false; } var loadWorld = AccessTools.Method(smType, "loadWorld", new[] { typeof(string), typeof(bool) }); if (loadWorld != null && !string.IsNullOrEmpty(path)) { // The boolean is pLoadWorkshop, not "start immediately". Passing true // dereferences currentWorkshopMapData and falls back to a random world. loadWorld.Invoke(instance, new object[] { path, false }); detail = $"loadWorld slot={slot} path={path}"; return true; } var start = AccessTools.Method(smType, "startLoadSlot", Type.EmptyTypes); if (start != null) { start.Invoke(instance, Array.Empty()); detail = $"startLoadSlot slot={slot} path={path}"; return true; } detail = $"no loadWorld/startLoadSlot path={path}"; return false; } catch (Exception ex) { detail = "load_failed: " + (ex.InnerException?.Message ?? ex.Message); return false; } } private static string Escape(string s) { if (string.IsNullOrEmpty(s)) { return ""; } return s.Replace("\\", "\\\\").Replace("\"", "\\\""); } private static void EnsureDirs() { string dir = HarnessDir(); if (string.IsNullOrEmpty(dir)) { return; } if (!Directory.Exists(dir)) { Directory.CreateDirectory(dir); } if (_offset == 0 && File.Exists(OffsetPath())) { if (long.TryParse(File.ReadAllText(OffsetPath()).Trim(), out long stored)) { _offset = stored; } } } /// /// Script drops .harness/reset (and/or writes offset=0) before a new command batch. /// Without this, a same-length rewrite of commands.jsonl is skipped forever. /// private static void CheckExternalReset() { string dir = HarnessDir(); if (string.IsNullOrEmpty(dir)) { return; } bool resetRequested = false; string resetPath = Path.Combine(dir, "reset"); if (File.Exists(resetPath)) { try { File.Delete(resetPath); } catch { // Still reset in memory. } resetRequested = true; } // Also honor an on-disk offset that was rewound by the runner. if (!resetRequested && File.Exists(OffsetPath())) { try { if (long.TryParse(File.ReadAllText(OffsetPath()).Trim(), out long diskOffset) && diskOffset < _offset) { resetRequested = true; _offset = diskOffset; } } catch { // ignore } } if (!resetRequested) { return; } Queue.Clear(); _waitUntil = -1f; _directorRunActive = false; _readyLogged = false; _offset = 0; _lastSpawned = null; _lastSpawnedAssetId = ""; _rememberedFocusKey = ""; _rememberedTip = ""; _rememberedRelatedId = 0; InterestDirector.SetHarnessFastTiming(false); SpeciesDiscovery.SetHarnessFastTiming(false); try { File.WriteAllText(OffsetPath(), "0"); } catch { // ignore } LogService.LogInfo("[IdleSpectator][HARNESS] inbox reset - re-reading commands.jsonl"); } private static void RequeueFront(HarnessCommand cmd) { List rest = new List(Queue); Queue.Clear(); Queue.Enqueue(cmd); foreach (HarnessCommand step in rest) { Queue.Enqueue(step); } } private static string PickSpawnAssetId() { foreach (string id in PreferredSpawnAssets) { if (AssetManager.actor_library?.get(id) != null) { return id; } } return null; } private static Actor ResolveUnit(string assetId) { if (_lastSpawned != null && _lastSpawned.isAlive()) { if (string.IsNullOrEmpty(assetId) || (_lastSpawned.asset != null && _lastSpawned.asset.id == assetId)) { return _lastSpawned; } } Vector3 pos = CameraPos(); if (!string.IsNullOrEmpty(assetId)) { return WorldActivityScanner.FindAliveByAssetId(assetId, pos, 5000f); } if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null && MoveCamera._focus_unit.isAlive()) { return MoveCamera._focus_unit; } return WorldActivityScanner.FindNearestAliveUnit(pos, 5000f); } private static bool RetryFront(HarnessCommand cmd, string reason, int maxRetries = 20) { cmd.retries++; if (cmd.retries > maxRetries) { return false; } RequeueFront(cmd); _waitUntil = Time.unscaledTime + 0.35f; if (cmd.retries == 1 || cmd.retries % 10 == 0) { LogService.LogInfo( $"[IdleSpectator][HARNESS] retry id={cmd.id} action={cmd.action} reason={reason} attempt={cmd.retries}"); } return true; } private static bool CaptureHarnessScreenshot(string fileName) { try { string dir = HarnessDir(); if (string.IsNullOrEmpty(dir)) { return false; } Directory.CreateDirectory(dir); string name = string.IsNullOrEmpty(fileName) ? "hud.png" : fileName; if (!name.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) { name += ".png"; } string path = Path.Combine(dir, name); // Absolute path required so Unity does not write under the game cwd. ScreenCapture.CaptureScreenshot(path); LastScreenshotPath = path; string cropPath = Path.Combine( dir, Path.GetFileNameWithoutExtension(name) + "-dossier.png"); if (ModClass.Instance != null) { ModClass.Instance.StartCoroutine(CaptureDossierCropEndOfFrame(cropPath)); } LogService.LogInfo( $"[IdleSpectator][HARNESS] screenshot queued path={path} crop={cropPath} layoutOk={WatchCaption.LastLayoutOk} size={WatchCaption.LastPanelSize}"); return true; } catch (Exception ex) { LastScreenshotPath = ""; LogService.LogInfo("[IdleSpectator][HARNESS] screenshot failed: " + ex.Message); return false; } } private static IEnumerator CaptureDossierCropEndOfFrame(string cropPath) { yield return new WaitForEndOfFrame(); try { if (!WatchCaption.TryGetScreenPixelRect(out Rect rect)) { LogService.LogInfo("[IdleSpectator][HARNESS] dossier crop skipped: no visible dossier rect"); yield break; } int x = Mathf.Clamp(Mathf.FloorToInt(rect.xMin), 0, Screen.width - 1); int y = Mathf.Clamp(Mathf.FloorToInt(rect.yMin), 0, Screen.height - 1); int w = Mathf.Clamp(Mathf.CeilToInt(rect.width), 1, Screen.width - x); int h = Mathf.Clamp(Mathf.CeilToInt(rect.height), 1, Screen.height - y); if (w < 16 || h < 16) { LogService.LogInfo($"[IdleSpectator][HARNESS] dossier crop skipped: tiny rect {w}x{h}"); yield break; } Texture2D tex = new Texture2D(w, h, TextureFormat.RGB24, false); tex.ReadPixels(new Rect(x, y, w, h), 0, 0); tex.Apply(); byte[] png = tex.EncodeToPNG(); UnityEngine.Object.Destroy(tex); File.WriteAllBytes(cropPath, png); LogService.LogInfo( $"[IdleSpectator][HARNESS] dossier crop written path={cropPath} rect=({x},{y},{w},{h}) screen={Screen.width}x{Screen.height}"); } catch (Exception ex) { LogService.LogInfo("[IdleSpectator][HARNESS] dossier crop failed: " + ex.Message); } } private static string HarnessDir() { if (string.IsNullOrEmpty(ModClass.ModFolder)) { return null; } return Path.Combine(ModClass.ModFolder, ".harness"); } private static string CommandsPath() => Path.Combine(HarnessDir(), "commands.jsonl"); private static string ResultsPath() => Path.Combine(HarnessDir(), "results.jsonl"); private static string LastResultPath() => Path.Combine(HarnessDir(), "last-result.json"); private static string OffsetPath() => Path.Combine(HarnessDir(), "offset"); }