using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; 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 bool _directorRunActive; private static string _rememberedFocusKey = ""; private static string LastScreenshotPath = ""; 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, 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 "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 tier={InterestDirector.CurrentTierName}"); break; } case "expire_grace": { InterestDirector.HarnessExpireInputGrace(); _cmdOk++; Emit(cmd, ok: true, detail: $"in_grace={InterestDirector.InInputGrace}"); break; } case "remember_focus": { _rememberedFocusKey = FocusKey(); bool ok = !string.IsNullOrEmpty(_rememberedFocusKey) && _rememberedFocusKey != "-"; if (ok) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: $"focus={_rememberedFocusKey}"); break; } case "spectator": { bool on = ParseBool(cmd.value, defaultValue: true); string expect = (cmd.expect ?? "").Trim().ToLowerInvariant(); 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) { _cmdOk++; } else { _cmdFail++; } long id = Chronicle.CurrentHistorySubjectId(); Emit(cmd, ok, detail: $"milestone={kind} history={Chronicle.HistoryCountFor(id)} activity={ActivityLog.CountFor(id)}"); 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_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_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": { Actor 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 alive = false; try { alive = focus != null && focus.isAlive(); } catch { alive = false; } bool applied = alive && StatusGameApi.TryAddStatus(focus, statusId, timer); bool ok = expectBlocked ? !applied : applied; if (ok) { _cmdOk++; } else { _cmdFail++; } WatchCaption.ForceRefreshHistory(); Emit( cmd, ok, detail: $"status={statusId} applied={applied} expectBlocked={expectBlocked} alive={alive} " + $"gains={ActivityLog.StatusGainsSinceClear} refreshes={ActivityLog.StatusRefreshesSinceClear} " + $"active={StatusGameApi.HasStatus(focus, statusId)}"); 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 "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 "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(); deathPos = focus.current_position; } 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) { _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)}"); 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 "set_setting": DoSetSetting(cmd); break; case "assert": DoAssert(cmd); break; case "snapshot": DoSnapshot(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) { string status = _assertFail == 0 && _cmdFail == 0 ? "PASS" : "FAIL"; WriteLastResult(status, $"ok={_cmdOk} fail={_cmdFail} assert_pass={_assertPass} assert_fail={_assertFail}"); LogService.LogInfo( $"[IdleSpectator][HARNESS] batch done status={status} ok={_cmdOk} fail={_cmdFail} assert_pass={_assertPass} assert_fail={_assertFail}"); } } 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; 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; Actor 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}"); } 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 = 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; } } 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; for (int i = 0; i < count; i++) { Actor actor = World.world.units.spawnNewUnit(assetId, tile, pSpawnSound: false, pMiracleSpawn: true); if (actor != null && actor.isAlive()) { spawned++; last = actor; } } if (spawned <= 0 || last == null || !last.isAlive()) { if (RetryFront(cmd, "spawn_zero")) { return; } _cmdFail++; Emit(cmd, ok: false, detail: $"spawned=0/{count} asset={assetId}"); return; } _lastSpawned = last; _lastSpawnedAssetId = last.asset != null ? last.asset.id : assetId; CameraDirector.FocusUnit(last); _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); _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; } InterestTier tier = ParseTier(cmd.tier, InterestTier.Curiosity); 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 { Tier = tier, Score = 50f, 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)) { 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; } InterestEvent interest = new InterestEvent { Tier = ParseTier(cmd.tier, InterestTier.Curiosity), Score = 40f, Position = follow.current_position, FollowUnit = follow, Label = string.IsNullOrEmpty(cmd.label) ? $"Harness {ParseTier(cmd.tier, InterestTier.Curiosity)}" : cmd.label.Replace("{asset}", follow.asset != null ? follow.asset.id : (assetId ?? "")), CreatedAt = Time.unscaledTime, AssetId = follow.asset != null ? follow.asset.id : assetId }; InterestCollector.EnqueueDirect(interest); _cmdOk++; Emit(cmd, ok: true, detail: $"queued tier={interest.Tier} label={interest.Label} pending={InterestCollector.PendingCount}"); } 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 val = ParseBool(cmd.value, defaultValue: true); bool ok = ModSettings.TrySetBool(key, val); if (ok) { _cmdOk++; } else { _cmdFail++; } Emit(cmd, ok, detail: $"{key}={val}"); } 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 "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_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 "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 : ""; 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 "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 "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": { string want = (cmd.value ?? cmd.tier ?? "").Trim(); string have = InterestDirector.CurrentTierName; pass = !string.IsNullOrEmpty(want) && string.Equals(have, want, System.StringComparison.OrdinalIgnoreCase); detail = $"tier={have} want={want} label={InterestDirector.CurrentLabel}"; 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} tier={InterestDirector.CurrentTierName}"; break; } case "focus_same": case "focus_unchanged": { string nowKey = FocusKey(); pass = !string.IsNullOrEmpty(_rememberedFocusKey) && _rememberedFocusKey != "-" && nowKey == _rememberedFocusKey; detail = $"remembered={_rememberedFocusKey} now={nowKey}"; 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 "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)}"; break; } case "dossier_task_refresh": { // Stale focus snapshot must recover: blank chip, then sync from live AI. Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; string live = UnitDossier.ReadLiveTask(focus); string after = WatchCaption.HarnessBlankAndRefreshTask(); string chip = WatchCaption.ShownTaskChipText; bool chipMatches = string.IsNullOrEmpty(live) ? string.IsNullOrEmpty(chip) : !string.IsNullOrEmpty(chip); pass = after == live && chipMatches; 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 : ""; pass = latest != null && !string.IsNullOrEmpty(needle) && line.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0; detail = $"latest='{display}' needle='{needle}' date='{(latest != null ? latest.DateLabel : "")}'"; 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 = 220f; 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 <= 3; 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_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 = WatchCaption.CurrentUnitId; 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}"; break; } case "activity_log_not_contains": { long id = WatchCaption.CurrentUnitId; 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_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_status_gain_count": { int want = ParseCountExpect(cmd, defaultValue: 1); int got = ActivityLog.StatusGainsSinceClear; string mode = (cmd.label ?? "exact").Trim().ToLowerInvariant(); pass = mode == "min" ? got >= want : got == want; detail = $"gains={got} want={want} mode={mode} 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 = ""; 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}'"; 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 line = latest != null ? latest.Line : ""; string lore = latest != null ? latest.HudLine : ""; string display = latest != null ? latest.DisplayLine : ""; pass = latest != null && latest.Kind != ChronicleKind.AgeChapter && !string.IsNullOrEmpty(needle) && (line.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0 || lore.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0); detail = $"world_latest='{display}' needle='{needle}' age='{Chronicle.CurrentAgeName}'"; 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; pass = Chronicle.HudVisible && have == want; detail = $"detailId={ChronicleHud.DetailUnitId} want={want} 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 "death_manner": { long id = ChronicleHud.DetailUnitId; DeathManner have = Chronicle.LatestDeathMannerFor(id); string haveLabel = Chronicle.DeathMannerLabel(have); string want = (cmd.value ?? "").Trim().ToLowerInvariant(); pass = id != 0 && (haveLabel == want || 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 != 0 && WatchCaption.CurrentUnitId != focusId; detail = $"fallenFocus={detailStr} focusId={focusId} dossierId={WatchCaption.CurrentUnitId}"; 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 void DoSnapshot(HarnessCommand cmd) { string snap = $"idle={SpectatorMode.Active} focus={MoveCamera.hasFocusUnit()} powerBar={CanvasMain.isBottomBarShowing()} unit={FocusLabel()} tip={CameraDirector.LastWatchLabel} tipAsset={CameraDirector.LastWatchAssetId} bad={StateProbe.BadEventCount} tier={InterestDirector.CurrentTierName} discoveryPending={SpeciesDiscovery.PendingCount} caption={WatchCaption.LastHeadline} chronicle={Chronicle.Count}"; _cmdOk++; Emit(cmd, ok: true, detail: snap); LogService.LogInfo("[IdleSpectator][SNAP] " + snap); } 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 { Tier = ParseTier(cmd.tier, InterestTier.Curiosity), 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 string SafeName(Actor actor) { try { string name = actor.getName(); if (!string.IsNullOrEmpty(name)) { return name; } } catch { // ignore } return actor.asset != null ? actor.asset.id : "unit"; } private static InterestTier ParseTier(string raw, InterestTier fallback) { if (string.IsNullOrEmpty(raw)) { return fallback; } if (Enum.TryParse(raw, ignoreCase: true, out InterestTier tier)) { return tier; } 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 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 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 = ""; 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"); }