worldbox-observer-mod/IdleSpectator/AgentHarness.cs
2026-07-16 15:29:28 -05:00

6746 lines
252 KiB
C#

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;
/// <summary>
/// Agent-driven in-game command bus. Drop JSON lines into <c>.harness/commands.jsonl</c>
/// (or a scenario via <c>{"action":"scenario","value":"smoke"}</c>). Results go to
/// <c>.harness/results.jsonl</c> and <c>.harness/last-result.json</c>.
/// </summary>
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<HarnessCommand> Queue = new Queue<HarnessCommand>();
private static Actor _lastSpawned;
private static string _lastSpawnedAssetId = "";
private static Actor _happinessPartner;
private static long _activitySubjectId;
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;
/// <summary>
/// When true, relationship interest feeds still Emit while <see cref="Busy"/>
/// (harness verification of live Harmony patches).
/// </summary>
public static bool ForceRelationshipFeeds;
/// <summary>
/// When true, InterestDirector / SpeciesDiscovery stay frozen during harness commands.
/// <c>director_run</c> temporarily clears this so dwell/interrupt/discovery can execute.
/// </summary>
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 score={InterestDirector.CurrentScoreLabel}");
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_force_headline":
{
string headline = !string.IsNullOrEmpty(cmd.value)
? cmd.value
: (!string.IsNullOrEmpty(cmd.label) ? cmd.label : "Zezqzo Tikzkek Tak (scorpion)");
WatchCaption.ForceNametagHeadline(headline);
_cmdOk++;
Emit(cmd, true, detail: $"headline='{WatchCaption.LastHeadline}' shown='{WatchCaption.ShownNameChipText}'");
break;
}
case "dossier_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 "hatch_emit":
{
Actor focus = ResolveUnit(
string.IsNullOrEmpty(cmd.asset) || cmd.asset == "auto" ? null : cmd.asset);
if (focus == null)
{
focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
}
bool alive = false;
try
{
alive = focus != null && focus.isAlive();
}
catch
{
alive = false;
}
if (alive)
{
InterestFeeds.EmitHatch(focus, "harness");
_cmdOk++;
}
else
{
_cmdFail++;
}
Emit(cmd, alive, detail: alive ? EventReason.Hatch(focus) : "no unit");
break;
}
case "status_clear_all":
{
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
bool ok = focus != null && StatusGameApi.TryFinishAll(focus);
if (ok)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
WatchCaption.ForceRefreshHistory();
Emit(
cmd,
ok,
detail:
$"cleared={ok} losses={ActivityLog.StatusLossesSinceClear} "
+ $"active_count={StatusGameApi.CountActiveStatuses(focus)}");
break;
}
case "status_force_note":
{
long id = WatchCaption.CurrentUnitId;
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
if (id == 0 && focus != null)
{
try
{
id = focus.getID();
}
catch
{
id = 0;
}
}
string statusId = !string.IsNullOrEmpty(cmd.value) ? cmd.value.Trim() : "zzz_harness_fake_status";
bool gained = (cmd.expect ?? "gain").IndexOf("loss", StringComparison.OrdinalIgnoreCase) < 0;
string actorName = "";
try
{
if (focus != null)
{
actorName = focus.getName() ?? "";
}
}
catch
{
actorName = "";
}
bool ok = ActivityLog.ForceStatusNote(id, statusId, gained, actorName);
WatchCaption.ForceRefreshHistory();
if (ok)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
Emit(cmd, ok, detail: $"forced status={statusId} gained={gained} id={id}");
break;
}
case "happiness_reset":
{
long id = 0;
try
{
if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null)
{
id = MoveCamera._focus_unit.getID();
}
}
catch
{
id = 0;
}
if (id == 0)
{
id = WatchCaption.CurrentUnitId;
}
HappinessEventRouter.ResetCountersForSubject(id);
ActivityLog.ResetSampleCounters();
_cmdOk++;
Emit(
cmd,
ok: true,
detail:
$"subject={id} notes={HappinessEventRouter.NotesSinceClear} "
+ $"ring={HappinessEventRouter.RingWritesSinceClear}");
break;
}
case "happiness_apply":
{
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
string effectId = !string.IsNullOrEmpty(cmd.value) ? cmd.value.Trim() : "just_ate";
int bonus = 0;
if (!string.IsNullOrEmpty(cmd.label)
&& int.TryParse(cmd.label, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsedBonus))
{
bonus = parsedBonus;
}
bool expectBlocked = (cmd.expect ?? "").IndexOf("block", StringComparison.OrdinalIgnoreCase) >= 0;
bool alive = false;
try
{
alive = focus != null && focus.isAlive();
}
catch
{
alive = false;
}
bool applied = alive && HappinessGameApi.TryChangeHappiness(focus, effectId, bonus);
bool ok = expectBlocked ? !applied : applied;
if (ok)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
WatchCaption.ForceRefreshHistory();
Emit(
cmd,
ok,
detail:
$"effect={effectId} applied={applied} expectBlocked={expectBlocked} "
+ $"notes={HappinessEventRouter.NotesSinceClear} ring={HappinessEventRouter.RingWritesSinceClear} "
+ $"last={HappinessEventRouter.LastEffectId}");
break;
}
case "happiness_force_note":
{
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
long id = 0;
try
{
if (focus != null)
{
id = focus.getID();
}
}
catch
{
id = 0;
}
// Prefer live focus over a stale dossier CurrentUnitId.
if (id == 0)
{
id = WatchCaption.CurrentUnitId;
}
string effectId = !string.IsNullOrEmpty(cmd.value) ? cmd.value.Trim() : "just_ate";
string related = cmd.label ?? "";
string actorName = "";
try
{
if (focus != null)
{
actorName = focus.getName() ?? "";
}
}
catch
{
actorName = "";
}
bool ok = ActivityLog.ForceHappinessNote(id, effectId, actorName, related);
Actor registerUnit = focus;
if (registerUnit == null && InterestDirector.CurrentCandidate != null)
{
registerUnit = InterestDirector.CurrentCandidate.FollowUnit;
}
if (registerUnit == null && id != 0)
{
foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic())
{
try
{
if (actor != null && actor.getID() == id)
{
registerUnit = actor;
break;
}
}
catch
{
// continue
}
}
}
if (registerUnit == null && MoveCamera.hasFocusUnit())
{
registerUnit = MoveCamera._focus_unit;
}
if (ok && registerUnit != null)
{
string label = string.IsNullOrEmpty(related)
? (actorName + " · " + effectId)
: (actorName + " mourns " + related);
InterestCandidate registered = InterestFeeds.RegisterHappinessSignal(
registerUnit, effectId, label, related);
if (registered == null)
{
ok = false;
}
}
else if (ok)
{
ok = false;
}
WatchCaption.ForceRefreshHistory();
if (ok)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
Emit(
cmd,
ok,
detail: $"forced happiness={effectId} related={related} id={id} unit={(registerUnit != null)} pending={InterestRegistry.PendingCount} count={InterestRegistry.Count}");
break;
}
case "happiness_inject_all":
{
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
bool alive = false;
try
{
alive = focus != null && focus.isAlive();
}
catch
{
alive = false;
}
if (!alive)
{
_cmdFail++;
Emit(cmd, ok: false, detail: "no living focus");
break;
}
List<string> ids = ActivityAssetCatalog.EnumerateLiveHappinessIds();
int applied = 0;
int blocked = 0;
for (int i = 0; i < ids.Count; i++)
{
if (HappinessGameApi.TryChangeHappiness(focus, ids[i], 0))
{
applied++;
}
else
{
blocked++;
}
}
WatchCaption.ForceRefreshHistory();
bool ok = ids.Count > 0 && applied + blocked == ids.Count;
if (ok)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
Emit(
cmd,
ok,
detail:
$"live={ids.Count} applied={applied} blocked={blocked} "
+ $"notes={HappinessEventRouter.NotesSinceClear}");
break;
}
case "happiness_remember_partner":
{
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : _lastSpawned;
_happinessPartner = focus;
bool ok = _happinessPartner != null;
if (ok)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
Emit(cmd, ok, detail: ok ? $"partner={SafeName(_happinessPartner)}" : "no partner");
break;
}
case "happiness_bond_lovers":
{
Actor a = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
Actor b = _happinessPartner;
bool ok = false;
try
{
if (a != null && b != null && a.isAlive() && b.isAlive() && a != b)
{
a.becomeLoversWith(b);
ok = true;
}
}
catch
{
ok = false;
}
if (ok)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
WatchCaption.ForceRefreshHistory();
Emit(
cmd,
ok,
detail:
$"a={SafeName(a)} b={SafeName(b)} notes={HappinessEventRouter.NotesSinceClear}");
break;
}
case "relationship_set_parent":
{
Actor child = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : _lastSpawned;
Actor parent = _happinessPartner;
bool ok = false;
string detail;
ForceRelationshipFeeds = true;
try
{
if (child != null && parent != null && child.isAlive() && parent.isAlive()
&& child != parent)
{
child.setParent1(parent);
ok = true;
detail = $"parent={SafeName(parent)} child={SafeName(child)}";
}
else
{
detail = "need focus child + remembered parent";
}
}
catch (Exception ex)
{
detail = "exception: " + ex.Message;
ok = false;
}
finally
{
ForceRelationshipFeeds = false;
}
if (ok)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
Emit(cmd, ok, detail: detail);
break;
}
case "happiness_bond_friends":
{
Actor a = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
Actor b = _happinessPartner;
bool ok = false;
try
{
if (a != null && b != null && a.isAlive() && b.isAlive() && a != b)
{
a.setBestFriend(b, true);
b.setBestFriend(a, true);
ok = true;
}
}
catch
{
ok = false;
}
if (ok)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
WatchCaption.ForceRefreshHistory();
Emit(
cmd,
ok,
detail:
$"a={SafeName(a)} b={SafeName(b)} notes={HappinessEventRouter.NotesSinceClear}");
break;
}
case "happiness_kill_partner":
{
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
Actor partner = _happinessPartner;
string err = "";
try
{
if (partner != null && partner.isAlive())
{
partner.die(true, AttackType.Divine);
}
}
catch (Exception ex)
{
err = ex.Message ?? "die_exception";
}
bool ok = false;
try
{
ok = partner != null && !partner.isAlive();
}
catch
{
ok = false;
}
// Keep the surviving focus so grief asserts resolve the right subject.
try
{
if (focus != null && focus.isAlive())
{
CameraDirector.FocusUnit(focus);
_activitySubjectId = focus.getID();
}
}
catch
{
// ignore
}
if (ok)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
WatchCaption.ForceRefreshHistory();
Emit(
cmd,
ok,
detail: $"killed={SafeName(partner)} ok={ok}"
+ (string.IsNullOrEmpty(err) ? "" : " err=" + err));
break;
}
case "happiness_burst":
{
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
string effectId = !string.IsNullOrEmpty(cmd.value) ? cmd.value.Trim() : "just_started_war";
int n = Math.Max(1, cmd.count > 0 ? cmd.count : 64);
bool alive = false;
try
{
alive = focus != null && focus.isAlive();
}
catch
{
alive = false;
}
if (!alive)
{
_cmdFail++;
Emit(cmd, ok: false, detail: "no living focus");
break;
}
float t0 = Time.realtimeSinceStartup;
int applied = 0;
for (int i = 0; i < n; i++)
{
if (HappinessGameApi.TryChangeHappiness(focus, effectId, 0))
{
applied++;
}
}
float ms = (Time.realtimeSinceStartup - t0) * 1000f;
HappinessEventRouter.FlushAggregatesNow();
bool ok = applied > 0 && ms < 250f;
if (ok)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
Emit(
cmd,
ok,
detail:
$"effect={effectId} n={n} applied={applied} ms={ms:0.0} "
+ $"agg={HappinessEventRouter.AggregateSummariesSinceClear}");
break;
}
case "kill_focus":
{
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
bool ok = false;
try
{
if (focus != null && focus.isAlive())
{
focus.die(true, AttackType.Age);
ok = !focus.isAlive();
}
}
catch
{
ok = false;
}
if (ok)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
Emit(cmd, ok, detail: $"killed={ok}");
break;
}
case "lore_detail_pane":
{
string raw = (cmd.value ?? cmd.label ?? "life").Trim().ToLowerInvariant();
ChronicleHud.DetailPane pane = raw.StartsWith("act")
? ChronicleHud.DetailPane.Activity
: ChronicleHud.DetailPane.Life;
ChronicleHud.SetDetailPane(pane);
bool ok = ChronicleHud.DetailUnitId != 0
&& ChronicleHud.ActiveDetailPane == pane;
if (ok)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
Emit(cmd, ok, detail:
$"pane={ChronicleHud.ActiveDetailPane} detailId={ChronicleHud.DetailUnitId} "
+ $"actRows={ChronicleHud.LastDetailActivityRows} lifeRows={ChronicleHud.LastDetailHistoryRows}");
break;
}
case "chronicle_world_force":
{
string label = !string.IsNullOrEmpty(cmd.label)
? cmd.label
: (!string.IsNullOrEmpty(cmd.value) ? cmd.value : "War: harness vs test");
bool ok = Chronicle.ForceWorldNote(label);
if (ok)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
Emit(cmd, ok, detail: $"world_count={Chronicle.WorldCount} label={label}");
break;
}
case "chronicle_death_sample":
{
string typeName = !string.IsNullOrEmpty(cmd.value) ? cmd.value : "Age";
bool ok = Chronicle.ForceDeathSample(typeName);
if (ok)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
ChronicleEntry latest = Chronicle.Latest;
Emit(cmd, ok, detail: $"type={typeName} latest={(latest != null ? latest.Line : "")}");
break;
}
case "chronicle_tab":
case "lore_tab":
case "world_memory_open":
{
Chronicle.ShowHud();
string tabRaw = (cmd.value ?? cmd.label ?? "world").Trim().ToLowerInvariant();
if (tabRaw == "fallen" || tabRaw == "dead")
{
ChronicleHud.SetTab(ChronicleHud.LoreTab.Fallen);
}
else if (tabRaw == "living"
|| tabRaw == "characters"
|| tabRaw == "character"
|| tabRaw == "chars"
|| tabRaw == "history")
{
ChronicleHud.SetTab(ChronicleHud.LoreTab.Living);
}
else
{
ChronicleHud.SetTab(ChronicleHud.LoreTab.World);
}
bool open = Chronicle.HudVisible;
if (open)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
Emit(cmd, open,
detail: $"hud={open} tab={ChronicleHud.ActiveTab} memory={Chronicle.MemoryCount} recent={Chronicle.RecentFocusCount}");
break;
}
case "lore_refresh":
case "chronicle_refresh":
{
ChronicleHud.RefreshCastList();
_cmdOk++;
Emit(
cmd,
ok: true,
detail:
$"stale={ChronicleHud.CastListStale} top='{ChronicleHud.LastListTopTitle}' tab={ChronicleHud.ActiveTab}");
break;
}
case "lore_search":
{
Chronicle.ShowHud();
ChronicleHud.SetTab(ChronicleHud.LoreTab.Living);
ChronicleHud.SetSearch(cmd.value ?? cmd.label ?? "");
_cmdOk++;
Emit(cmd, ok: true, detail: $"search='{ChronicleHud.SearchText}'");
break;
}
case "lore_favorites":
{
Chronicle.ShowHud();
string v = (cmd.value ?? "true").Trim().ToLowerInvariant();
bool on = v != "false" && v != "0" && v != "off";
ChronicleHud.SetFavoritesOnly(on);
// Filter is a list view - leave detail if we were in one.
if (ChronicleHud.DetailUnitId != 0)
{
ChronicleHud.ClearUnitDetail();
}
_cmdOk++;
Emit(
cmd,
ok: true,
detail: $"favoritesOnly={ChronicleHud.FavoritesOnly} tab={ChronicleHud.ActiveTab}");
break;
}
case "lore_pick":
{
Chronicle.ShowHud();
ChronicleHud.SetTab(ChronicleHud.LoreTab.Living);
string needle = cmd.value ?? cmd.label ?? "";
if (needle == "auto")
{
needle = "";
}
bool ok = ChronicleHud.PickFirstCharacter(needle);
if (ok)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
Emit(cmd, ok,
detail: $"picked={ok} focus={MoveCamera.hasFocusUnit()} dossier={WatchCaption.LastHeadline} recent={Chronicle.RecentFocusCount}");
break;
}
case "lore_open_focus":
case "chronicle_open_focus":
{
ChronicleHud.OpenSyncedFromFocus();
bool open = Chronicle.HudVisible;
if (open)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
Emit(cmd, open,
detail:
$"lore={Chronicle.HudVisible} detail={ChronicleHud.DetailUnitId} dossierId={WatchCaption.CurrentUnitId} idle={SpectatorMode.Active} follow={ChronicleHud.FollowFocus}");
break;
}
case "lore_pick_fallen":
{
Chronicle.ShowHud();
ChronicleHud.SetTab(ChronicleHud.LoreTab.Fallen);
string needle = cmd.value ?? cmd.label ?? "";
if (needle == "auto")
{
needle = "";
}
bool ok = ChronicleHud.PickFirstFallen(needle);
if (ok)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
Emit(cmd, ok,
detail:
$"picked={ok} detail={ChronicleHud.DetailUnitId} follow={ChronicleHud.FollowFocus} hist={Chronicle.HistoryCountFor(ChronicleHud.DetailUnitId)} fallenFocus={Chronicle.LastFallenFocusDetail} manner={Chronicle.LatestDeathMannerFor(ChronicleHud.DetailUnitId)}");
break;
}
case "chronicle_orphan":
{
int n = Math.Max(1, cmd.count);
string name = !string.IsNullOrEmpty(cmd.label) ? cmd.label : "Orphan";
string species = !string.IsNullOrEmpty(cmd.asset) ? cmd.asset : "creature";
string prefix = !string.IsNullOrEmpty(cmd.value) ? cmd.value : "Fallen";
AttackType attackType = AttackType.Age;
if (!string.IsNullOrEmpty(cmd.expect)
&& System.Enum.TryParse(cmd.expect, ignoreCase: true, out AttackType parsedAttack))
{
attackType = parsedAttack;
}
long killerId = 0;
Vector3 deathPos = Vector3.zero;
string mode = (cmd.tier ?? "").Trim().ToLowerInvariant();
bool favorite = mode.IndexOf("fav", System.StringComparison.Ordinal) >= 0;
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
if (mode.IndexOf("killer", System.StringComparison.Ordinal) >= 0
&& focus != null
&& focus.isAlive())
{
killerId = focus.getID();
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<string>();
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<ChronicleEntry> snap = Chronicle.SnapshotMemory();
for (int i = snap.Count - 1; i >= 0; i--)
{
ChronicleEntry e = snap[i];
if (e == null || e.Kind == ChronicleKind.AgeChapter)
{
continue;
}
if (e.Kind == ChronicleKind.World || e.IsLegend)
{
target = e;
break;
}
}
bool ok = target != null && Chronicle.JumpTo(target);
if (ok)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
Emit(
cmd,
ok,
detail:
$"jumped={ok} detail={Chronicle.LastJumpDetail} focus={MoveCamera.hasFocusUnit()} line={(target != null ? target.Line : "")}");
break;
}
case "screenshot":
{
bool ok = CaptureHarnessScreenshot(cmd.value);
if (ok)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
Emit(cmd, ok, detail: $"path={LastScreenshotPath} layoutOk={WatchCaption.LastLayoutOk} size={WatchCaption.LastPanelSize}");
break;
}
case "spawn":
DoSpawn(cmd);
break;
case "pick_unit":
DoPickUnit(cmd);
break;
case "focus":
DoFocus(cmd);
break;
case "watch":
DoWatch(cmd);
break;
case "trigger_interest":
DoTriggerInterest(cmd);
break;
case "world_log_feed":
DoWorldLogFeed(cmd);
break;
case "domain_feed":
DoDomainFeed(cmd);
break;
case "interest_inject":
DoInterestInject(cmd);
break;
case "interest_force_session":
DoInterestForceSession(cmd);
break;
case "interest_variety_clear":
InterestVariety.Clear();
_cmdOk++;
Emit(cmd, ok: true, detail: "variety_cleared");
break;
case "scoring_reload":
{
bool ok = InterestScoringConfig.Reload();
bool catalogOk = EventCatalogConfig.Reload();
if (ok)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
Emit(
cmd,
ok: ok,
detail: ok
? $"loaded v={InterestScoringConfig.LoadedVersion} path={InterestScoringConfig.LoadedPath} "
+ $"eventCatalog={catalogOk} decisions={EventCatalogConfig.DecisionCount}"
: "scoring_reload_failed");
break;
}
case "event_catalog_reload":
{
bool ok = EventCatalogConfig.Reload();
if (ok)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
Emit(
cmd,
ok: ok,
detail: ok
? $"loaded v={EventCatalogConfig.LoadedVersion} path={EventCatalogConfig.LoadedPath}"
: "event_catalog_reload_failed");
break;
}
case "interest_variety_seed":
{
// value = "events:chars" e.g. "2:10"
string raw = (cmd.value ?? "").Trim();
int events = 0;
int chars = 0;
int colon = raw.IndexOf(':');
if (colon >= 0)
{
int.TryParse(raw.Substring(0, colon), NumberStyles.Integer, CultureInfo.InvariantCulture, out events);
int.TryParse(raw.Substring(colon + 1), NumberStyles.Integer, CultureInfo.InvariantCulture, out chars);
}
else
{
int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out events);
}
InterestVariety.HarnessSeedMix(events, chars);
_cmdOk++;
Emit(
cmd,
ok: true,
detail: $"seeded events={events} chars={chars} share={InterestVariety.EventShare():0.##} samples={InterestVariety.MixSampleCount}");
break;
}
case "interest_end_session":
InterestDirector.HarnessEndCurrent(cmd.value ?? "harness");
_cmdOk++;
Emit(
cmd,
ok: true,
detail: $"ended key={InterestDirector.CurrentKey} interrupted={InterestDirector.InterruptedKey}");
break;
case "interest_release_force":
InterestDirector.HarnessReleaseForceActive();
_cmdOk++;
Emit(cmd, ok: true, detail: $"released key={InterestDirector.CurrentKey} active={InterestDirector.CurrentIsActive}");
break;
case "interest_mark_inactive":
{
float ago = ParseFloat(cmd.value, 1f);
InterestDirector.HarnessMarkInactive(ago);
// Rebuild dossier so orange reason clears under quiet_grace / inactive dwell.
if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null)
{
WatchCaption.SetFromActor(MoveCamera._focus_unit);
}
_cmdOk++;
Emit(
cmd,
ok: true,
detail: $"inactive_for={ago:0.##}s active={InterestDirector.CurrentIsActive} key={InterestDirector.CurrentKey} reason='{WatchCaption.Current?.ReasonLine}'");
break;
}
case "interest_clear_follow":
{
Actor related = ResolveUnit(string.IsNullOrEmpty(cmd.asset) || cmd.asset == "auto" ? null : cmd.asset);
if (related == null)
{
related = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f);
}
// Prefer a different living unit than the current follow.
Actor follow = InterestDirector.CurrentCandidate?.FollowUnit;
if (related != null && follow != null && related == follow)
{
foreach (Actor actor in WorldActivityScanner.EnumerateAliveUnitsPublic())
{
if (actor != null && actor != follow)
{
try
{
if (actor.isAlive())
{
related = actor;
break;
}
}
catch
{
// continue
}
}
}
}
bool ok = InterestDirector.HarnessClearFollowForHandoff(related);
if (ok)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
Emit(cmd, ok, detail: $"cleared related={SafeName(related)} key={InterestDirector.CurrentKey}");
break;
}
case "interest_expire_pending":
{
string needle = cmd.value ?? "";
InterestRegistry.ForceExpireContaining(needle, Time.unscaledTime);
_cmdOk++;
Emit(cmd, ok: true, detail: $"expired needle='{needle}' pending={InterestRegistry.PendingCount}");
break;
}
case "interest_feeds_tick":
{
int drained = InterestFeeds.HarnessDrainOnce();
_cmdOk++;
Emit(
cmd,
ok: true,
detail: $"drained={drained} cursor={InterestFeeds.HappinessCursor} pending={InterestRegistry.PendingCount}");
break;
}
case "interest_peek_next":
{
InterestCandidate peek = InterestDirector.HarnessPeekNext();
_cmdOk++;
Emit(
cmd,
ok: true,
detail: peek == null
? "peek=null"
: $"peek={peek.Key} lead={peek.LeadKind} score={peek.TotalScore:0.#} label={peek.Label}");
break;
}
case "interest_civic_boost":
{
string effectId = !string.IsNullOrEmpty(cmd.value) ? cmd.value.Trim() : "just_started_war";
string kingdom = !string.IsNullOrEmpty(cmd.label) ? cmd.label : "harness";
float amount = ParseFloat(cmd.expect, 40f);
InterestFeeds.HarnessStampCivicBoost(kingdom, effectId, amount);
_cmdOk++;
Emit(cmd, ok: true, detail: $"civic kingdom={kingdom} effect={effectId} amt={amount:0.#}");
break;
}
case "happiness_suppress":
{
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
if (focus == null && InterestDirector.CurrentCandidate != null)
{
focus = InterestDirector.CurrentCandidate.FollowUnit;
}
if (focus == null)
{
focus = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f);
}
string effectId = !string.IsNullOrEmpty(cmd.value) ? cmd.value.Trim() : "death_child";
if (focus == null)
{
_cmdFail++;
Emit(cmd, ok: false, detail: "no unit for happiness_suppress");
break;
}
HappinessEventRouter.PublishSuppressed(focus, effectId, HappinessSourceHook.Harness);
_cmdOk++;
Emit(
cmd,
ok: true,
detail: $"suppressed={effectId} unit={SafeName(focus)} seq={HappinessEventRouter.CurrentSequence}");
break;
}
case "happiness_remember_only":
{
// Remember a Signal for DrainSince without immediate Ingest/Register.
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
if (focus == null && InterestDirector.CurrentCandidate != null)
{
focus = InterestDirector.CurrentCandidate.FollowUnit;
}
if (focus == null)
{
focus = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f);
}
string effectId = !string.IsNullOrEmpty(cmd.value) ? cmd.value.Trim() : "death_lover";
if (focus == null || !focus.isAlive())
{
_cmdFail++;
Emit(cmd, ok: false, detail: "no living unit for happiness_remember_only");
break;
}
HappinessOccurrence published = HappinessEventRouter.PublishApplied(
focus,
effectId,
HappinessGameApi.ResolveAmount(effectId),
HappinessSourceHook.Harness,
related: null,
role: HappinessRelationRole.None,
forceRing: true);
bool ok = published != null;
if (ok)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
Emit(
cmd,
ok,
detail: $"remembered={effectId} seq={HappinessEventRouter.CurrentSequence} cursor={InterestFeeds.HappinessCursor} unit={SafeName(focus)}");
break;
}
case "set_setting":
DoSetSetting(cmd);
break;
case "assert":
DoAssert(cmd);
break;
case "snapshot":
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<HarnessCommand> 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<HarnessCommand> rest = new List<HarnessCommand>(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);
try
{
_activitySubjectId = unit.getID();
}
catch
{
_activitySubjectId = 0;
}
_cmdOk++;
Emit(cmd, ok: true, detail: $"unit={SafeName(unit)} asset={(unit.asset != null ? unit.asset.id : "?")}");
}
private static void DoWatch(HarnessCommand cmd)
{
if (!WorldReady())
{
if (RetryFront(cmd, "world_not_ready"))
{
return;
}
_cmdFail++;
Emit(cmd, ok: false, detail: "world_not_ready");
return;
}
string assetId = cmd.asset;
if (assetId == "auto")
{
assetId = _lastSpawnedAssetId;
}
Actor follow = ResolveUnit(string.IsNullOrEmpty(assetId) ? null : assetId);
if (follow == null)
{
if (RetryFront(cmd, string.IsNullOrEmpty(assetId) ? "no_unit" : "no_asset_unit"))
{
return;
}
_cmdFail++;
Emit(cmd, ok: false, detail: string.IsNullOrEmpty(assetId) ? "no unit for watch" : "no unit for asset: " + assetId);
return;
}
float tierEvt = EventStrengthFromTierHint(cmd.tier, 40f);
InterestLeadKind tierLead = LeadFromTierHint(cmd.tier, InterestLeadKind.CharacterLed);
string resolvedAsset = follow.asset != null ? follow.asset.id : assetId;
string label = string.IsNullOrEmpty(cmd.label)
? $"Harness: {resolvedAsset}"
: cmd.label.Replace("{asset}", resolvedAsset ?? "");
if (label.Contains("{asset}"))
{
label = label.Replace("{asset}", resolvedAsset ?? "");
}
InterestEvent interest = new InterestEvent
{
Score = tierEvt,
Position = follow.current_position,
FollowUnit = follow,
Label = label,
CreatedAt = Time.unscaledTime,
AssetId = follow.asset != null ? follow.asset.id : assetId
};
CameraDirector.Watch(interest);
_cmdOk++;
Emit(cmd, ok: true, detail: $"tip={label} unit={SafeName(follow)}");
}
private static void DoTriggerInterest(HarnessCommand cmd)
{
if (!WorldReady())
{
_cmdFail++;
Emit(cmd, ok: false, detail: "world_not_ready");
return;
}
string assetId = cmd.asset;
Actor follow = null;
if (!string.IsNullOrEmpty(assetId))
{
follow = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f, assetId);
}
if (follow == null)
{
follow = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f);
}
if (follow == null)
{
_cmdFail++;
Emit(cmd, ok: false, detail: "no unit for interest");
return;
}
float tierEvt = EventStrengthFromTierHint(cmd.tier, 40f);
InterestLeadKind tierLead = LeadFromTierHint(cmd.tier, InterestLeadKind.CharacterLed);
string tipLabel = string.IsNullOrEmpty(cmd.label)
? $"Harness {cmd.tier ?? "scene"}"
: cmd.label.Replace("{asset}", follow.asset != null ? follow.asset.id : (assetId ?? ""));
InterestEvent interest = new InterestEvent
{
Score = tierEvt,
Position = follow.current_position,
FollowUnit = follow,
Label = tipLabel,
CreatedAt = Time.unscaledTime,
AssetId = follow.asset != null ? follow.asset.id : assetId
};
bool eventLedNotice = tierLead == InterestLeadKind.EventLed
&& tierEvt >= InterestScoringConfig.W.noticeScoreMin;
InterestCandidate registered = InterestFeeds.RegisterHarness(
follow,
interest.Label,
keySuffix: (interest.Label ?? "scene").Replace(" ", "_"),
lead: tierLead,
completion: InterestCompletionKind.FixedDwell,
forceActive: false,
eventStrength: tierEvt,
maxWatch: tierEvt >= 95f ? 40f : 25f);
if (registered == null)
{
InterestCollector.EnqueueDirect(interest);
}
else if (eventLedNotice)
{
// Only force the camera when score-margin cut-in would allow it (matches director policy).
InterestScoring.ScoreCheap(registered);
float cur = InterestDirector.CurrentScore;
bool noCurrent = InterestDirector.CurrentCandidate == null;
bool marginCut = registered.TotalScore >= cur + InterestScoringConfig.W.cutInMargin;
if (noCurrent || marginCut)
{
InterestDirector.HarnessForceSession(registered);
CameraDirector.Watch(registered.ToInterestEvent());
}
}
_cmdOk++;
Emit(
cmd,
ok: true,
detail: $"triggered label={tipLabel} evt={tierEvt:0.#} forced={InterestDirector.CurrentKey != null && InterestDirector.CurrentKey.IndexOf((interest.Label ?? "").Replace(" ", "_"), System.StringComparison.OrdinalIgnoreCase) >= 0}");
}
/// <summary>
/// Feed a live WorldLog asset id through InterestFeeds as if WorldLog fired (coverage of authored catalog).
/// </summary>
private static void DoWorldLogFeed(HarnessCommand cmd)
{
if (!WorldReady())
{
_cmdFail++;
Emit(cmd, ok: false, detail: "world_not_ready");
return;
}
string assetId = string.IsNullOrEmpty(cmd.asset) ? (cmd.value ?? "") : cmd.asset;
if (string.IsNullOrEmpty(assetId) || assetId == "auto")
{
assetId = "disaster_tornado";
}
if (!EventCatalog.WorldLog.HasAuthored(assetId))
{
_cmdFail++;
Emit(cmd, ok: false, detail: "not_authored:" + assetId);
return;
}
WorldLogEventEntry entry = EventCatalog.WorldLog.GetOrFallback(assetId);
Actor follow = ResolveUnit(null) ?? WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f);
if (follow == null && entry.CreatesInterest)
{
_cmdFail++;
Emit(cmd, ok: false, detail: "no unit for world_log_feed");
return;
}
if (!entry.CreatesInterest)
{
_cmdOk++;
Emit(cmd, ok: true, detail: $"skipped_no_interest id={assetId}");
return;
}
string kingdom = follow.kingdom != null ? (follow.kingdom.name ?? "Alpha") : "Alpha";
string key = "worldlog:" + assetId + ":" + kingdom;
var candidate = new InterestCandidate
{
Key = key,
LeadKind = InterestLeadKind.EventLed,
Category = entry.Category,
Source = "worldlog",
EventStrength = entry.EventStrength,
VisualConfidence = 0.7f,
Position = follow.current_position,
FollowUnit = follow,
SubjectId = follow.getID(),
Label = entry.MakeLabel(kingdom, "Beta"),
AssetId = assetId,
KingdomKey = kingdom,
SpeciesId = follow.asset != null ? follow.asset.id : "",
CreatedAt = Time.unscaledTime,
LastSeenAt = Time.unscaledTime,
ExpiresAt = Time.unscaledTime + 40f,
MinWatch = entry.MinWatch,
MaxWatch = entry.MaxWatch,
Completion = InterestCompletionKind.FixedDwell
};
InterestScoring.ScoreCheap(candidate);
EventFeedUtil.RegisterCandidate(candidate);
_cmdOk++;
Emit(cmd, ok: true, detail: $"fed id={assetId} key={key} evt={entry.EventStrength:0.#}");
}
/// <summary>
/// Injects a Signal from a domain catalog while the harness is Busy
/// (domain feeds themselves early-out on Busy, so register here).
/// asset = domain, value = event/asset id.
/// </summary>
private static void DoDomainFeed(HarnessCommand cmd)
{
if (!WorldReady())
{
_cmdFail++;
Emit(cmd, ok: false, detail: "world_not_ready");
return;
}
string domain = (cmd.asset ?? "").Trim().ToLowerInvariant();
string id = (cmd.value ?? cmd.expect ?? "").Trim();
Actor unit = ResolveUnit(null) ?? WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f);
if (unit == null && domain != "era")
{
_cmdFail++;
Emit(cmd, ok: false, detail: "no unit for domain_feed");
return;
}
try
{
DiscreteEventEntry entry;
string key;
string label;
float strength;
string category;
string source;
switch (domain)
{
case "relationship":
case "rel":
if (string.IsNullOrEmpty(id))
{
id = "set_lover";
}
entry = EventCatalog.Relationship.GetOrFallback(id);
Actor related = ActorRelation.ResolvePackRelated(unit)
?? WorldActivityScanner.FindNearestAliveUnit(unit.current_position, 40f);
if (related == unit)
{
related = null;
}
key = "rel:" + entry.Id + ":" + EventFeedUtil.SafeId(unit) + ":"
+ EventFeedUtil.SafeId(related);
label = entry.MakeLabel(
EventFeedUtil.SafeName(unit),
EventFeedUtil.SafeName(related));
strength = entry.EventStrength;
category = entry.Category;
source = "relationship";
InterestCandidate relReg = EventFeedUtil.Register(
key,
source,
category,
label,
id,
strength,
unit.current_position,
unit,
related: related);
if (relReg == null)
{
_cmdFail++;
Emit(cmd, ok: false, detail: "register_failed domain=relationship id=" + id);
return;
}
_cmdOk++;
Emit(cmd, ok: true, detail: $"domain=relationship id={id} key={key}");
return;
case "plot":
if (string.IsNullOrEmpty(id))
{
id = "new_war";
}
entry = EventCatalog.Plot.GetOrFallback(id);
key = "plot:" + entry.Id + ":new:" + EventFeedUtil.SafeId(unit);
label = EventReason.Plot(unit, entry.LabelTemplate, "new", entry.Id);
strength = entry.EventStrength;
category = entry.Category;
source = "plot";
break;
case "era":
if (string.IsNullOrEmpty(id))
{
id = "age_chaos";
}
entry = EventCatalog.Era.GetOrFallback(id);
key = "era:" + entry.Id;
label = entry.MakeLabel("", "");
strength = entry.EventStrength;
category = entry.Category;
source = "era";
Vector3 eraPos = unit != null
? unit.current_position
: (Camera.main != null
? new Vector3(Camera.main.transform.position.x, Camera.main.transform.position.y, 0f)
: new Vector3(1f, 1f, 0f));
InterestCandidate eraReg = EventFeedUtil.Register(
key,
source,
category,
label,
id,
strength,
eraPos,
follow: unit,
locationOnly: unit == null);
if (eraReg == null)
{
_cmdFail++;
Emit(cmd, ok: false, detail: "register_failed domain=era id=" + id);
return;
}
_cmdOk++;
Emit(cmd, ok: true, detail: $"domain=era id={id} key={key}");
return;
case "book":
if (string.IsNullOrEmpty(id))
{
id = "history_book";
}
entry = EventCatalog.Book.GetOrFallback(id);
key = "book:" + entry.Id + ":new:" + EventFeedUtil.SafeId(unit);
label = entry.MakeLabel(EventFeedUtil.SafeName(unit), "new");
strength = entry.EventStrength;
category = entry.Category;
source = "book";
break;
case "trait":
if (string.IsNullOrEmpty(id))
{
id = "immortal";
}
entry = EventCatalog.Trait.GetOrFallback(id);
key = "trait:" + entry.Id + ":gains:" + EventFeedUtil.SafeId(unit);
label = EventReason.Trait(unit, entry.Id, gained: true);
strength = entry.EventStrength;
category = entry.Category;
source = "trait";
break;
case "meta":
if (string.IsNullOrEmpty(id))
{
id = "new_city";
}
key = "meta:" + id + ":" + EventFeedUtil.SafeId(unit);
label = EventReason.MetaNew(unit, id);
strength = 74f;
category = "Politics";
source = "meta";
break;
case "boat":
id = "boat_unload";
key = "boat:boat_unload:" + EventFeedUtil.SafeId(unit);
label = EventReason.Boat(unit, "unload");
strength = 72f;
category = "Travel";
source = "boat";
break;
case "building":
id = "building_destroy";
key = "building:destroy:harness:" + EventFeedUtil.SafeId(unit);
label = EventReason.BuildingFalls("harness_hall", unit);
strength = 88f;
category = "Spectacle";
source = "building";
break;
case "spell":
if (string.IsNullOrEmpty(id))
{
id = "summon_lightning";
}
{
DiscreteEventEntry spellEntry = EventCatalog.Spell.GetOrFallback(id);
key = "spell:" + spellEntry.Id + ":" + EventFeedUtil.SafeId(unit);
label = spellEntry.MakeLabel(EventFeedUtil.SafeName(unit), "");
strength = spellEntry.EventStrength;
category = spellEntry.Category;
source = "spell";
id = spellEntry.Id;
}
break;
case "item":
if (string.IsNullOrEmpty(id))
{
id = "sword";
}
{
DiscreteEventEntry itemEntry = EventCatalog.Item.GetOrFallback(id);
key = "item:" + itemEntry.Id + ":" + EventFeedUtil.SafeId(unit);
label = itemEntry.MakeLabel(EventFeedUtil.SafeName(unit), "");
strength = itemEntry.EventStrength;
category = itemEntry.Category;
source = "item";
id = itemEntry.Id;
}
break;
case "decision":
if (string.IsNullOrEmpty(id))
{
id = "declare_war";
}
{
DiscreteEventEntry decEntry = EventCatalog.Decision.GetOrFallback(id);
key = "decision:" + decEntry.Id + ":" + EventFeedUtil.SafeId(unit);
label = decEntry.MakeLabel(EventFeedUtil.SafeName(unit), "");
strength = decEntry.EventStrength;
category = decEntry.Category;
source = "decision";
id = decEntry.Id;
}
break;
case "power":
if (string.IsNullOrEmpty(id))
{
id = "lightning";
}
{
DiscreteEventEntry powEntry = EventCatalog.Power.GetOrFallback(id);
key = "power:" + powEntry.Id + ":" + EventFeedUtil.SafeId(unit);
label = powEntry.MakeLabel(EventFeedUtil.SafeName(unit), "");
strength = powEntry.EventStrength;
category = powEntry.Category;
source = "power";
id = powEntry.Id;
}
break;
case "subspecies_trait":
case "subspecies":
if (string.IsNullOrEmpty(id))
{
id = "biome_trait";
}
{
DiscreteEventEntry stEntry = EventCatalog.SubspeciesTrait.GetOrFallback(id);
key = "subspecies_trait:" + stEntry.Id + ":" + EventFeedUtil.SafeId(unit);
label = stEntry.MakeLabel(EventFeedUtil.SafeName(unit), "");
strength = stEntry.EventStrength;
category = stEntry.Category;
source = "subspecies_trait";
id = stEntry.Id;
}
break;
case "gene":
if (string.IsNullOrEmpty(id))
{
id = "warfare_1";
}
{
DiscreteEventEntry geneEntry = EventCatalog.Gene.GetOrFallback(id);
key = "gene:" + geneEntry.Id + ":" + EventFeedUtil.SafeId(unit);
label = geneEntry.MakeLabel(EventFeedUtil.SafeName(unit), "");
strength = geneEntry.EventStrength;
category = geneEntry.Category;
source = "gene";
id = geneEntry.Id;
}
break;
default:
_cmdFail++;
Emit(cmd, ok: false, detail: "unknown domain=" + domain);
return;
}
InterestCandidate registered = EventFeedUtil.Register(
key,
source,
category,
label,
id,
strength,
unit.current_position,
unit);
if (registered == null)
{
_cmdFail++;
Emit(cmd, ok: false, detail: "register_failed domain=" + domain + " id=" + id);
return;
}
_cmdOk++;
Emit(cmd, ok: true, detail: $"domain={domain} id={id} key={key}");
}
catch (System.Exception ex)
{
_cmdFail++;
Emit(cmd, ok: false, detail: "domain_feed error: " + ex.Message);
}
}
private static void DoInterestInject(HarnessCommand cmd)
{
if (!WorldReady())
{
_cmdFail++;
Emit(cmd, ok: false, detail: "world_not_ready");
return;
}
Actor follow = ResolveUnit(string.IsNullOrEmpty(cmd.asset) || cmd.asset == "auto" ? null : cmd.asset);
if (follow == null)
{
follow = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f);
}
if (follow == null)
{
_cmdFail++;
Emit(cmd, ok: false, detail: "no unit for interest_inject");
return;
}
float tierEvt = EventStrengthFromTierHint(cmd.tier, 70f);
InterestLeadKind tierLead = LeadFromTierHint(cmd.tier, InterestLeadKind.EventLed);
string label = string.IsNullOrEmpty(cmd.label) ? ("Inject " + (cmd.tier ?? "scene")) : cmd.label;
string keySuffix = string.IsNullOrEmpty(cmd.expect) ? label.Replace(" ", "_") : cmd.expect;
ParseInterestInjectOptions(
cmd.value,
out bool force,
out InterestLeadKind? leadOpt,
out float eventStrength,
out float charSig,
out float maxWatch,
out float ttl,
out bool resumable,
out int participants,
out int notables);
InterestLeadKind lead = leadOpt ?? tierLead;
if (eventStrength < 0f)
{
eventStrength = tierEvt;
}
if (charSig < 0f)
{
charSig = lead == InterestLeadKind.CharacterLed ? eventStrength : 10f;
}
if (maxWatch < 0f)
{
maxWatch = 30f;
}
if (ttl < 0f)
{
ttl = 60f;
}
InterestCandidate c = InterestFeeds.RegisterHarness(
follow,
label,
keySuffix,
lead,
force ? InterestCompletionKind.Manual : InterestCompletionKind.FixedDwell,
force,
eventStrength,
charSig,
maxWatch,
ttl,
related: null,
resumable);
if (c == null)
{
_cmdFail++;
Emit(cmd, ok: false, detail: "inject_failed");
return;
}
if (participants > 0 || notables > 0)
{
c.ParticipantCount = participants;
c.NotableParticipantCount = notables;
if (participants >= 2 || c.AssetId == "live_battle")
{
c.Category = "Combat";
c.Verb = "fighting";
// Keep FixedDwell/Manual for harness injects - units are not actually fighting.
if (!force)
{
c.Completion = InterestCompletionKind.FixedDwell;
}
}
InterestScoring.RecalcTotal(c);
}
if (force)
{
InterestDirector.HarnessForceSession(c);
}
_cmdOk++;
Emit(
cmd,
ok: true,
detail: $"injected key={c.Key} score={c.TotalScore:0.#} lead={c.LeadKind} evt={c.EventStrength:0.#} char={c.CharacterSignificance:0.#} n={c.ParticipantCount}/{c.NotableParticipantCount} score={c.TotalScore:0.#} pending={InterestRegistry.PendingCount}");
}
private static void ParseInterestInjectOptions(
string raw,
out bool force,
out InterestLeadKind? lead,
out float eventStrength,
out float charSig,
out float maxWatch,
out float ttl,
out bool resumable,
out int participants,
out int notables)
{
force = false;
lead = null;
eventStrength = -1f;
charSig = -1f;
maxWatch = -1f;
ttl = -1f;
resumable = true;
participants = 0;
notables = 0;
if (string.IsNullOrEmpty(raw))
{
return;
}
string[] parts = raw.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < parts.Length; i++)
{
string p = parts[i].Trim();
if (p.Equals("true", StringComparison.OrdinalIgnoreCase)
|| p.Equals("force", StringComparison.OrdinalIgnoreCase))
{
force = true;
continue;
}
if (p.Equals("false", StringComparison.OrdinalIgnoreCase))
{
force = false;
continue;
}
int eq = p.IndexOf('=');
if (eq <= 0)
{
if (p.Equals("event", StringComparison.OrdinalIgnoreCase)
|| p.Equals("eventled", StringComparison.OrdinalIgnoreCase))
{
lead = InterestLeadKind.EventLed;
}
else if (p.Equals("character", StringComparison.OrdinalIgnoreCase)
|| p.Equals("char", StringComparison.OrdinalIgnoreCase)
|| p.Equals("characterled", StringComparison.OrdinalIgnoreCase))
{
lead = InterestLeadKind.CharacterLed;
}
continue;
}
string key = p.Substring(0, eq).Trim().ToLowerInvariant();
string val = p.Substring(eq + 1).Trim();
switch (key)
{
case "force":
force = ParseBool(val, false);
break;
case "lead":
if (val.StartsWith("c", StringComparison.OrdinalIgnoreCase))
{
lead = InterestLeadKind.CharacterLed;
}
else
{
lead = InterestLeadKind.EventLed;
}
break;
case "evt":
case "event":
case "strength":
eventStrength = ParseFloat(val, eventStrength);
break;
case "char":
case "charsig":
charSig = ParseFloat(val, charSig);
break;
case "max":
case "maxwatch":
maxWatch = ParseFloat(val, maxWatch);
break;
case "ttl":
ttl = ParseFloat(val, ttl);
break;
case "resumable":
resumable = ParseBool(val, true);
break;
case "n":
case "participants":
case "fighters":
int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out participants);
break;
case "notables":
case "notable":
int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out notables);
break;
}
}
}
private static void DoInterestForceSession(HarnessCommand cmd)
{
if (!WorldReady())
{
_cmdFail++;
Emit(cmd, ok: false, detail: "world_not_ready");
return;
}
Actor follow = ResolveUnit(string.IsNullOrEmpty(cmd.asset) || cmd.asset == "auto" ? null : cmd.asset);
if (follow == null)
{
follow = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f);
}
if (follow == null)
{
_cmdFail++;
Emit(cmd, ok: false, detail: "no unit for interest_force_session");
return;
}
float tierEvt = EventStrengthFromTierHint(cmd.tier, 70f);
InterestLeadKind tierLead = LeadFromTierHint(cmd.tier, InterestLeadKind.EventLed);
ParseInterestInjectOptions(
cmd.value,
out bool forceIgnored,
out InterestLeadKind? leadOpt,
out float eventStrength,
out float charSigIgnored,
out float maxWatchIgnored,
out float ttlIgnored,
out bool resumableIgnored,
out int participantsIgnored,
out int notablesIgnored);
_ = forceIgnored;
_ = charSigIgnored;
_ = maxWatchIgnored;
_ = ttlIgnored;
_ = resumableIgnored;
_ = participantsIgnored;
_ = notablesIgnored;
InterestLeadKind lead = leadOpt ?? tierLead;
if (eventStrength < 0f)
{
eventStrength = tierEvt;
}
string label = string.IsNullOrEmpty(cmd.label) ? ("Force " + (cmd.tier ?? "scene")) : cmd.label;
string focusName = SafeName(follow);
if (!string.IsNullOrEmpty(focusName))
{
label = label.Replace("{a}", focusName);
}
string keySuffix = string.IsNullOrEmpty(cmd.expect) ? "force_" + label.Replace(" ", "_") : cmd.expect;
InterestCandidate c = InterestFeeds.RegisterHarness(
follow,
label,
keySuffix,
lead: lead,
completion: InterestCompletionKind.Manual,
forceActive: true,
eventStrength: eventStrength > 0f ? eventStrength : 70f,
maxWatch: 60f);
if (c != null && lead == InterestLeadKind.CharacterLed)
{
c.Category = "CharacterVignette";
InterestScoring.ScoreCheap(c);
}
bool ok = InterestDirector.HarnessForceSession(c);
if (ok)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
Emit(cmd, ok, detail: $"key={InterestDirector.CurrentKey} score={InterestDirector.CurrentScoreLabel} active={InterestDirector.CurrentIsActive}");
}
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":
{
// Legacy name: prefer tip/label match; else score band from old tier names.
string want = cmd.value ?? "";
string label = InterestDirector.CurrentLabel ?? "";
string tip = CameraDirector.LastWatchLabel ?? "";
float score = InterestDirector.CurrentScore;
bool byText = (!string.IsNullOrEmpty(want)
&& (label.IndexOf(want, StringComparison.OrdinalIgnoreCase) >= 0
|| tip.IndexOf(want, StringComparison.OrdinalIgnoreCase) >= 0));
bool byBand = false;
string band = want.Trim().ToLowerInvariant();
if (band == "ambient")
{
byBand = InterestScoring.IsFillScore(score);
}
else if (band == "curiosity")
{
byBand = score >= 35f && score < InterestScoringConfig.W.noticeScoreMin;
}
else if (band == "action")
{
byBand = score >= InterestScoringConfig.W.noticeScoreMin;
}
else if (band == "story")
{
byBand = score >= 75f;
}
else if (band == "epic")
{
byBand = InterestScoring.IsHotScore(score) || score >= 95f;
}
else if (band == "none")
{
byBand = InterestDirector.CurrentCandidate == null;
}
pass = byText || byBand;
detail = $"want='{want}' label='{label}' tip='{tip}' score={score:0.#} text={byText} band={byBand}";
break;
}
case "would_accept_curiosity":
{
bool want = ParseBool(cmd.value, defaultValue: true);
bool have = InterestDirector.WouldAcceptCuriosity();
pass = have == want;
detail = $"would_accept={have} want={want} score={InterestDirector.CurrentScoreLabel}";
break;
}
case "focus_same":
case "focus_unchanged":
{
string nowKey = FocusKey();
pass = !string.IsNullOrEmpty(_rememberedFocusKey)
&& _rememberedFocusKey != "-"
&& nowKey == _rememberedFocusKey;
detail = $"remembered={_rememberedFocusKey} now={nowKey}";
break;
}
case "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 "scoring_config_loaded":
{
bool want = string.IsNullOrEmpty(cmd.value) || ParseBool(cmd.value, true);
bool have = InterestScoringConfig.LoadedFromFile;
pass = have == want;
detail =
$"loaded={have} want={want} v={InterestScoringConfig.LoadedVersion} charEvent={InterestScoringConfig.W.charWeightEventLed:0.##}";
break;
}
case "session_key":
case "interest_session_key":
{
string want = (cmd.value ?? cmd.expect ?? "").Trim();
string have = InterestDirector.CurrentKey ?? "";
pass = !string.IsNullOrEmpty(want)
&& have.IndexOf(want, System.StringComparison.OrdinalIgnoreCase) >= 0;
detail = $"key='{have}' contains='{want}'";
break;
}
case "session_active":
case "interest_session_active":
{
bool want = ParseBool(cmd.value, defaultValue: true);
bool have = InterestDirector.CurrentIsActive;
pass = have == want;
detail = $"active={have} want={want} key={InterestDirector.CurrentKey}";
break;
}
case "event_share":
case "interest_event_share":
{
float want = ParseFloat(cmd.value, 0.5f);
float have = InterestVariety.EventShare();
float tol = 0.35f;
if (!string.IsNullOrEmpty(cmd.label)
&& float.TryParse(cmd.label, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsedTol))
{
tol = parsedTol;
}
pass = InterestVariety.MixSampleCount < 3 || Mathf.Abs(have - want) <= tol;
detail = $"share={have:0.##} want~={want:0.##} tol={tol:0.##} samples={InterestVariety.MixSampleCount}";
break;
}
case "happiness_drain_seq":
{
ulong have = HappinessEventRouter.CurrentSequence;
pass = have >= 0;
detail = $"happiness_seq={have}";
break;
}
case "happiness_cursor":
{
ulong have = InterestFeeds.HappinessCursor;
if (!string.IsNullOrEmpty(cmd.value)
&& ulong.TryParse(cmd.value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong want))
{
pass = have == want;
detail = $"cursor={have} want={want}";
}
else
{
pass = true;
detail = $"cursor={have}";
}
break;
}
case "interest_has_key":
{
string want = (cmd.value ?? cmd.expect ?? "").Trim();
bool have = false;
string found = "";
if (!string.IsNullOrEmpty(want))
{
if ((InterestDirector.CurrentKey ?? "").IndexOf(want, System.StringComparison.OrdinalIgnoreCase) >= 0)
{
have = true;
found = InterestDirector.CurrentKey;
}
else
{
var pending = new System.Collections.Generic.List<InterestCandidate>(64);
InterestRegistry.CopyPending(pending);
for (int i = 0; i < pending.Count; i++)
{
InterestCandidate c = pending[i];
if (c?.Key != null
&& c.Key.IndexOf(want, System.StringComparison.OrdinalIgnoreCase) >= 0)
{
have = true;
found = c.Key;
break;
}
if (c?.HappinessEffectId != null
&& c.HappinessEffectId.IndexOf(want, System.StringComparison.OrdinalIgnoreCase) >= 0)
{
have = true;
found = c.Key;
break;
}
}
if (!have && InterestRegistry.CountKeysContaining(want) > 0)
{
have = true;
found = "registry:" + want;
}
}
}
pass = have;
detail = $"want='{want}' found='{found}' pending={InterestRegistry.PendingCount}";
break;
}
case "interest_label_contains":
{
string want = (cmd.value ?? cmd.expect ?? "").Trim();
bool have = false;
string found = "";
if (!string.IsNullOrEmpty(want))
{
string currentLabel = InterestDirector.CurrentLabel ?? "";
if (currentLabel.IndexOf(want, StringComparison.OrdinalIgnoreCase) >= 0)
{
have = true;
found = currentLabel;
}
else
{
var pending = new System.Collections.Generic.List<InterestCandidate>(64);
InterestRegistry.CopyPending(pending);
for (int i = 0; i < pending.Count; i++)
{
InterestCandidate c = pending[i];
string label = c?.Label ?? "";
if (label.IndexOf(want, StringComparison.OrdinalIgnoreCase) >= 0)
{
have = true;
found = label;
break;
}
}
}
}
pass = have;
detail = $"want='{want}' found='{found}' pending={InterestRegistry.PendingCount}";
break;
}
case "interest_no_key":
{
string want = (cmd.value ?? cmd.expect ?? "").Trim();
int n = InterestRegistry.CountKeysContaining(want);
bool inCurrent = !string.IsNullOrEmpty(want)
&& (InterestDirector.CurrentKey ?? "")
.IndexOf(want, StringComparison.OrdinalIgnoreCase) >= 0;
pass = n == 0 && !inCurrent;
detail = $"want_absent='{want}' count={n} current={InterestDirector.CurrentKey}";
break;
}
case "interest_registry_count":
{
string needle = cmd.label ?? "";
int want = ParseCountExpect(cmd, defaultValue: 0);
int have = string.IsNullOrEmpty(needle)
? InterestRegistry.Count
: InterestRegistry.CountKeysContaining(needle);
pass = have == want;
detail = $"count={have} want={want} needle='{needle}'";
break;
}
case "interrupted_key":
case "interest_interrupted":
{
string want = (cmd.value ?? "").Trim();
string have = InterestDirector.InterruptedKey ?? "";
if (string.IsNullOrEmpty(want))
{
pass = !string.IsNullOrEmpty(have);
}
else if (want.Equals("empty", StringComparison.OrdinalIgnoreCase)
|| want.Equals("none", StringComparison.OrdinalIgnoreCase))
{
pass = string.IsNullOrEmpty(have);
}
else
{
pass = have.IndexOf(want, StringComparison.OrdinalIgnoreCase) >= 0;
}
detail = $"interrupted='{have}' want='{want}'";
break;
}
case "interest_lead":
{
string want = (cmd.value ?? "EventLed").Trim();
InterestLeadKind? have = InterestDirector.CurrentLeadKind;
pass = have.HasValue
&& have.Value.ToString().Equals(want, StringComparison.OrdinalIgnoreCase);
detail = $"lead={have} want={want} key={InterestDirector.CurrentKey}";
break;
}
case "interest_peek_lead":
{
string want = (cmd.value ?? "EventLed").Trim();
InterestCandidate peek = InterestDirector.HarnessPeekNext();
pass = peek != null
&& peek.LeadKind.ToString().Equals(want, StringComparison.OrdinalIgnoreCase);
detail = peek == null
? "peek=null"
: $"peek_lead={peek.LeadKind} want={want} key={peek.Key}";
break;
}
case "interest_score_order":
{
// value = winnerNeedle, label = loserNeedle — winner TotalScore must be >= loser.
string winnerNeedle = (cmd.value ?? "").Trim();
string loserNeedle = (cmd.label ?? "").Trim();
InterestCandidate win = null;
InterestCandidate lose = null;
var all = new System.Collections.Generic.List<InterestCandidate>(64);
InterestRegistry.CopyPending(all);
if (InterestDirector.CurrentCandidate != null)
{
all.Add(InterestDirector.CurrentCandidate);
}
for (int i = 0; i < all.Count; i++)
{
InterestCandidate c = all[i];
if (c?.Key == null)
{
continue;
}
if (win == null
&& c.Key.IndexOf(winnerNeedle, StringComparison.OrdinalIgnoreCase) >= 0)
{
win = c;
}
if (lose == null
&& c.Key.IndexOf(loserNeedle, StringComparison.OrdinalIgnoreCase) >= 0)
{
lose = c;
}
}
pass = win != null && lose != null && win.TotalScore >= lose.TotalScore;
detail = $"win={win?.Key}({win?.TotalScore:0.#}) lose={lose?.Key}({lose?.TotalScore:0.#})";
break;
}
case "civic_boost":
{
string effectId = (cmd.value ?? "").Trim();
bool want = string.IsNullOrEmpty(cmd.label) || ParseBool(cmd.label, true);
bool have = InterestFeeds.HasCivicBoostContaining(effectId);
pass = have == want;
detail = $"civic={have} want={want} effect='{effectId}' count={InterestFeeds.CivicBoostCount}";
break;
}
case "in_grace":
{
bool want = ParseBool(cmd.value, defaultValue: true);
bool have = InterestDirector.InInputGrace;
pass = have == want;
detail = $"in_grace={have} want={want}";
break;
}
case "dossier_contains":
case "caption_contains":
{
string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value;
if (needle == "auto" || string.IsNullOrEmpty(needle))
{
needle = _lastSpawnedAssetId;
}
string caption = WatchCaption.LastCaptionText ?? "";
UnitDossier dossier = WatchCaption.Current;
bool dossierHit = dossier != null && dossier.ContainsIgnoreCase(needle);
bool captionHit = !string.IsNullOrEmpty(needle)
&& caption.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0;
pass = dossierHit || captionHit;
detail = $"needle='{needle}' caption='{caption.Replace("\n", " | ")}' dossier={(dossier != null)} reason='{dossier?.ReasonLine}'";
break;
}
case "dossier_not_contains":
{
string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value;
UnitDossier dossier = WatchCaption.Current;
string reason = dossier?.ReasonLine ?? "";
string caption = WatchCaption.LastCaptionText ?? "";
bool hit = (!string.IsNullOrEmpty(needle) && reason.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0)
|| (!string.IsNullOrEmpty(needle) && caption.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0)
|| (dossier != null && dossier.ContainsIgnoreCase(needle));
// Prefer reason-row check: fail only if the reason line (or full dossier) still shows the bad phrase.
bool reasonHit = !string.IsNullOrEmpty(needle)
&& reason.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0;
pass = !reasonHit;
detail = $"needle_absent_in_reason='{needle}' reason='{reason}' hit={reasonHit}";
break;
}
case "reason_owns_focus":
{
// Reason must not name a different live unit than the owned active candidate.
UnitDossier dossier = WatchCaption.Current;
string reason = dossier?.ReasonLine ?? "";
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
InterestCandidate owned = focus != null
? InterestDirector.TryGetOwnedReasonCandidate(focus)
: null;
if (string.IsNullOrEmpty(reason))
{
pass = true;
detail = "empty_reason_ok active=" + InterestDirector.CurrentIsActive;
break;
}
if (owned == null || focus == null)
{
pass = false;
detail = $"reason_without_owned_active focus={(focus != null)} active={InterestDirector.CurrentIsActive} reason='{reason}'";
break;
}
string stranger = FindStrangerNamedInReason(reason, owned);
pass = string.IsNullOrEmpty(stranger);
detail = pass
? $"owned reason='{reason}'"
: $"stranger_in_reason='{stranger}' reason='{reason}'";
break;
}
case "reason_empty":
{
UnitDossier dossier = WatchCaption.Current;
string reason = dossier?.ReasonLine ?? "";
bool wantEmpty = string.IsNullOrEmpty(cmd.value) || ParseBool(cmd.value, true);
bool isEmpty = string.IsNullOrEmpty(reason);
pass = isEmpty == wantEmpty;
detail = $"reason='{reason}' empty={isEmpty} wantEmpty={wantEmpty} active={InterestDirector.CurrentIsActive} quiet={InterestDirector.InQuietGrace}";
break;
}
case "dossier_task_refresh":
{
// Stale focus snapshot must recover: blank dossier TaskText, then sync from live AI.
// Empty live AI is ok - chip may keep a sticky prior label (never cleared on gaps).
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
string live = UnitDossier.ReadLiveTask(focus);
string after = WatchCaption.HarnessBlankAndRefreshTask();
string chip = WatchCaption.ShownTaskChipText;
if (string.IsNullOrEmpty(live))
{
pass = string.IsNullOrEmpty(after);
detail = $"live_empty dossier='{after}' sticky_chip='{chip}'";
break;
}
pass = after == live && !string.IsNullOrEmpty(chip);
detail = $"live='{live}' dossier='{after}' chip='{chip}'";
break;
}
case "chronicle_count":
{
int want = ParseCountExpect(cmd, defaultValue: 1);
int have = Chronicle.HistoryCountFor(Chronicle.CurrentHistorySubjectId());
string cmp = (cmd.label ?? "").Trim().ToLowerInvariant();
if (cmp == "min" || cmp == ">=" || string.IsNullOrEmpty(cmp))
{
pass = have >= want;
detail = $"history_count={have} min={want}";
}
else if (cmp == "world")
{
have = Chronicle.WorldCount;
pass = have >= want;
detail = $"world_count={have} min={want}";
}
else
{
pass = have == want;
detail = $"history_count={have} want={want}";
}
break;
}
case "chronicle_latest_contains":
{
string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value;
if (needle == "auto")
{
needle = _lastSpawnedAssetId;
}
ChronicleEntry latest = Chronicle.Latest;
string line = latest != null ? latest.Line : "";
string display = latest != null ? latest.DisplayLine : "";
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<ActivityEntry> entries = ActivityLog.LatestForSubject(id, 12);
var distinct = new HashSet<string>(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<ActivityEntry> 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("<color=", System.StringComparison.OrdinalIgnoreCase) >= 0)
{
colored = true;
break;
}
}
pass = any && colored;
detail = $"any={any} colored={colored} preview='{WatchCaption.LastHistoryPreview}'";
break;
}
case "activity_log_contains":
{
long id = ResolveActivitySubjectId();
string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value;
IReadOnlyList<ActivityEntry> entries =
ActivityLog.LatestForSubject(id, ActivityLog.MaxLinesPerSubject);
bool hit = false;
string sample = "";
for (int i = 0; i < entries.Count; i++)
{
ActivityEntry e = entries[i];
if (e == null)
{
continue;
}
string line = e.DisplayLine ?? e.Line ?? "";
if (string.IsNullOrEmpty(sample))
{
sample = line;
}
if (!string.IsNullOrEmpty(needle)
&& line.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0)
{
hit = true;
sample = line;
break;
}
}
pass = hit;
detail = $"hit={hit} needle='{needle}' sample='{sample}' count={entries.Count} id={id}";
break;
}
case "activity_log_not_contains":
{
long id = ResolveActivitySubjectId();
string needle = string.IsNullOrEmpty(cmd.value) ? cmd.label : cmd.value;
IReadOnlyList<ActivityEntry> entries =
ActivityLog.LatestForSubject(id, ActivityLog.MaxLinesPerSubject);
bool hit = false;
string sample = "";
for (int i = 0; i < entries.Count; i++)
{
ActivityEntry e = entries[i];
if (e == null)
{
continue;
}
string line = e.DisplayLine ?? e.Line ?? "";
if (string.IsNullOrEmpty(sample))
{
sample = line;
}
if (!string.IsNullOrEmpty(needle)
&& line.IndexOf(needle, System.StringComparison.OrdinalIgnoreCase) >= 0)
{
hit = true;
sample = line;
break;
}
}
pass = !hit;
detail = $"absent={!hit} needle='{needle}' sample='{sample}' count={entries.Count}";
break;
}
case "activity_verb_dedupe":
{
// Distinct play-like task ids should collapse to one Activity line within the cooldown.
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
if (focus == null || !focus.isAlive())
{
pass = false;
detail = "no_live_focus";
break;
}
ActivityLog.ResetSampleCounters();
string[] keys =
{
"task_unit_play",
"child_play",
"fun_move",
"child_run_around",
"frolic"
};
int added = ActivityLog.HarnessSpamSameVerb(focus, keys);
int suppressed = ActivityLog.VerbSuppressedSinceClear;
pass = added == 1 && suppressed >= keys.Length - 1;
detail =
$"added={added} want_added=1 suppressed={suppressed} want_suppressed>={keys.Length - 1} keys={keys.Length}";
break;
}
case "activity_verb_exempt":
{
// Combat / high-signal verbs skip same-verb cooldown.
Actor focus = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
if (focus == null || !focus.isAlive())
{
pass = false;
detail = "no_live_focus";
break;
}
ActivityLog.ResetSampleCounters();
string[] keys =
{
"task_attack",
"punch_a_building",
"tantrum",
"task_unit_attack"
};
int added = ActivityLog.HarnessSpamSameVerb(focus, keys);
int suppressed = ActivityLog.VerbSuppressedSinceClear;
pass = added == keys.Length && suppressed == 0;
detail =
$"added={added} want_added={keys.Length} suppressed={suppressed} want_suppressed=0";
break;
}
case "activity_prose_sentence":
{
// Ensure mood/task labels become sentences, not "{name} laughing".
long id = WatchCaption.CurrentUnitId;
string actorName = "";
try
{
if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null)
{
actorName = MoveCamera._focus_unit.getName() ?? "";
}
}
catch
{
actorName = "";
}
ActivityProse.Format(
ActivityKind.TaskStart,
"laughing",
actorName,
"",
false,
"",
"Laughing",
id,
99,
out string plain,
out _);
bool ok = !string.IsNullOrEmpty(plain)
&& plain.IndexOf("laugh", System.StringComparison.OrdinalIgnoreCase) >= 0
&& !plain.Equals(
(string.IsNullOrEmpty(actorName) ? "Someone" : actorName) + " laughing",
System.StringComparison.Ordinal);
pass = ok;
detail = $"plain='{plain}' actor='{actorName}'";
break;
}
case "activity_prose_alive":
{
// Generic tasks must read as living diary lines, not "{name} wanders".
string actorName = "Dorraa";
ActivityProse.Format(
ActivityKind.TaskStart,
"random_move",
ActivityContext.ForHarness(actorName, "", "buffalo"),
"Wandering",
11,
1,
out string moveLine,
out _);
ActivityProse.Format(
ActivityKind.TaskStart,
"wait",
ActivityContext.ForHarness(actorName, "", "buffalo"),
"Waiting",
12,
1,
out string waitLine,
out _);
ActivityProse.Format(
ActivityKind.TaskStart,
"city_idle_walking",
ActivityContext.ForHarness(actorName, "", "human", place: "Riverhold", jobId: "farmer"),
"Walking",
13,
1,
out string walkLine,
out _);
bool moveOk = !string.IsNullOrEmpty(moveLine)
&& moveLine.Length >= 28
&& !moveLine.Equals(actorName + " wanders", System.StringComparison.Ordinal)
&& (moveLine.IndexOf("wander", System.StringComparison.OrdinalIgnoreCase) >= 0
|| moveLine.IndexOf("drift", System.StringComparison.OrdinalIgnoreCase) >= 0
|| moveLine.IndexOf("roam", System.StringComparison.OrdinalIgnoreCase) >= 0
|| moveLine.IndexOf("ambl", System.StringComparison.OrdinalIgnoreCase) >= 0
|| moveLine.IndexOf("trudg", System.StringComparison.OrdinalIgnoreCase) >= 0
|| moveLine.IndexOf("move", System.StringComparison.OrdinalIgnoreCase) >= 0);
bool waitOk = !string.IsNullOrEmpty(waitLine)
&& waitLine.Length >= 18
&& !waitLine.Equals(actorName + " waits", System.StringComparison.Ordinal);
bool walkOk = !string.IsNullOrEmpty(walkLine)
&& walkLine.IndexOf("Riverhold", System.StringComparison.OrdinalIgnoreCase) < 0
&& walkLine.IndexOf("farmer", System.StringComparison.OrdinalIgnoreCase) < 0
&& walkLine.IndexOf(", a ", System.StringComparison.OrdinalIgnoreCase) < 0
&& walkLine.Length >= 28;
pass = moveOk && waitOk && walkOk;
detail =
$"moveOk={moveOk} waitOk={waitOk} walkOk={walkOk} "
+ $"move='{moveLine}' wait='{waitLine}' walk='{walkLine}'";
break;
}
case "activity_prose_species":
{
// Species stays on the dossier nametag. Lines must diverge by manner / taxonomy
// without spelling the species noun in the activity text.
string actorName = "Tester";
try
{
if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null)
{
actorName = MoveCamera._focus_unit.getName() ?? "Tester";
}
}
catch
{
actorName = "Tester";
}
ActivityProse.Format(
ActivityKind.TaskStart,
"task_unit_play",
ActivityContext.ForHarness(actorName, "", "dog"),
"Playing",
1,
1,
out string dogLine,
out _);
ActivityProse.Format(
ActivityKind.TaskStart,
"task_unit_play",
ActivityContext.ForHarness(actorName, "", "cat"),
"Playing",
2,
1,
out string catLine,
out _);
ActivityProse.Format(
ActivityKind.TaskStart,
"task_unit_play",
ActivityContext.ForHarness(actorName, "", "human", place: "Riverhold"),
"Playing",
3,
1,
out string humanLine,
out _);
ActivityProse.Format(
ActivityKind.ActionBeat,
"BehUnloadResources",
ActivityContext.ForHarness(
actorName,
"",
"human",
place: "Riverhold",
carrying: "wheat"),
"Unloads wheat",
4,
1,
out string unloadLine,
out _);
// Species must never leak into the place clause (wild kingdoms often = species id).
ActivityContext buffaloBadPlace = ActivityContext.ForHarness(
"Dorraa",
"",
"buffalo",
place: "buffalo");
buffaloBadPlace.PlaceLabel = ActivityInterestTable.PickPlaceLabel(
"buffalo",
"",
"buffalo",
"buffalo");
ActivityProse.Format(
ActivityKind.TaskStart,
"task_unit_play",
buffaloBadPlace,
"Playing",
5,
1,
out string buffaloLine,
out _);
ActivityContext buffaloGoodPlace = ActivityContext.ForHarness(
"Dorraa",
"",
"buffalo",
place: "Riverhold");
ActivityProse.Format(
ActivityKind.TaskStart,
"task_unit_play",
buffaloGoodPlace,
"Playing",
6,
1,
out string buffaloTownLine,
out _);
bool dogOk = !string.IsNullOrEmpty(dogLine)
&& dogLine.Length >= 16
&& dogLine.IndexOf(" the dog", System.StringComparison.OrdinalIgnoreCase) < 0;
bool catOk = !string.IsNullOrEmpty(catLine)
&& catLine.Length >= 16
&& catLine.IndexOf(" the cat", System.StringComparison.OrdinalIgnoreCase) < 0;
bool humanOk = !string.IsNullOrEmpty(humanLine)
&& !humanLine.Equals(dogLine, System.StringComparison.Ordinal)
&& !humanLine.Equals(catLine, System.StringComparison.Ordinal)
&& humanLine.IndexOf(" the human", System.StringComparison.OrdinalIgnoreCase) < 0;
bool unloadOk = !string.IsNullOrEmpty(unloadLine)
&& unloadLine.IndexOf("wheat", System.StringComparison.OrdinalIgnoreCase) >= 0
&& unloadLine.IndexOf("Riverhold", System.StringComparison.OrdinalIgnoreCase) >= 0
&& unloadLine.IndexOf("while carrying", System.StringComparison.OrdinalIgnoreCase) < 0;
bool distinct = !dogLine.Equals(catLine, System.StringComparison.Ordinal);
bool placeRejectOk = !string.IsNullOrEmpty(buffaloLine)
&& buffaloLine.IndexOf(" in buffalo", System.StringComparison.OrdinalIgnoreCase) < 0
&& buffaloLine.IndexOf(" the buffalo", System.StringComparison.OrdinalIgnoreCase) < 0;
// Play is not place-centric - city must not appear on play lines.
bool placeKeepOk = !string.IsNullOrEmpty(buffaloTownLine)
&& buffaloTownLine.IndexOf("Riverhold", System.StringComparison.OrdinalIgnoreCase) < 0
&& humanLine.IndexOf("Riverhold", System.StringComparison.OrdinalIgnoreCase) < 0;
ActivityProse.Format(
ActivityKind.ActionBeat,
"BehSocializeTalk",
ActivityContext.ForHarness(actorName, "Tomi", "human", place: "Celuford"),
"Talks",
11,
1,
out string socialLine,
out _);
bool socialBindOk = !string.IsNullOrEmpty(socialLine)
&& socialLine.IndexOf("Tomi", System.StringComparison.OrdinalIgnoreCase) >= 0
&& socialLine.IndexOf("with another", System.StringComparison.OrdinalIgnoreCase) < 0
&& socialLine.IndexOf("nearby company", System.StringComparison.OrdinalIgnoreCase) < 0;
int withTomi = 0;
int withIdx = 0;
while (withIdx >= 0)
{
withIdx = socialLine.IndexOf(
"with Tomi",
withIdx,
System.StringComparison.OrdinalIgnoreCase);
if (withIdx < 0)
{
break;
}
withTomi++;
withIdx += "with Tomi".Length;
}
socialBindOk = socialBindOk && withTomi <= 1;
// Building/place labels must never become companion "with …" on work/unload.
// Objects use "at the …"; settlements use "in …".
ActivityContext bonfireCtx = ActivityContext.ForHarness(
actorName,
"",
"human",
place: "bonfire",
placeIsObject: true);
ActivityProse.Format(
ActivityKind.TaskStart,
"type_bonfire",
bonfireCtx,
"Works a bonfire",
12,
1,
out string bonfireLine,
out _);
bool bonfireOk = !string.IsNullOrEmpty(bonfireLine)
&& bonfireLine.IndexOf("at the bonfire", System.StringComparison.OrdinalIgnoreCase) >= 0
&& bonfireLine.IndexOf("with bonfire", System.StringComparison.OrdinalIgnoreCase) < 0
&& bonfireLine.IndexOf("in bonfire", System.StringComparison.OrdinalIgnoreCase) < 0;
ActivityContext unloadTownCtx = ActivityContext.ForHarness(
actorName,
"",
"human",
place: "Riverhold",
carrying: "wheat");
ActivityProse.Format(
ActivityKind.ActionBeat,
"BehUnloadResources",
unloadTownCtx,
"Unloads wheat",
13,
1,
out string unloadTownLine,
out _);
bool unloadTownOk = !string.IsNullOrEmpty(unloadTownLine)
&& unloadTownLine.IndexOf("wheat", System.StringComparison.OrdinalIgnoreCase) >= 0
&& unloadTownLine.IndexOf("in Riverhold", System.StringComparison.OrdinalIgnoreCase) >= 0
&& unloadTownLine.IndexOf("with town", System.StringComparison.OrdinalIgnoreCase) < 0;
// Taxonomy kingdoms like "insect" must never become place clauses.
string insectPlace = ActivityInterestTable.PickPlaceLabel(
"insect",
"",
"insect",
"beetle");
ActivityContext beetleCtx = ActivityContext.ForHarness("Eeeew", "", "beetle", place: insectPlace);
beetleCtx.PlaceLabel = insectPlace;
ActivityProse.Format(
ActivityKind.TaskStart,
"task_unit_play",
beetleCtx,
"Playing",
7,
1,
out string beetleLine,
out _);
bool insectRejectOk = string.IsNullOrEmpty(insectPlace)
&& !string.IsNullOrEmpty(beetleLine)
&& beetleLine.IndexOf(" in insect", System.StringComparison.OrdinalIgnoreCase) < 0
&& beetleLine.IndexOf(" the beetle", System.StringComparison.OrdinalIgnoreCase) < 0;
ActivityProse.Format(
ActivityKind.TaskStart,
"task_unit_play",
ActivityContext.ForHarness("Creaker", "", "frog"),
"Playing",
8,
1,
out string frogLine,
out _);
ActivityProse.Format(
ActivityKind.TaskStart,
"child_random_jump",
ActivityContext.ForHarness("Creaker", "", "frog"),
"Jumping",
9,
1,
out string frogJumpLine,
out _);
bool frogOk = !string.IsNullOrEmpty(frogLine)
&& frogLine.Length >= 16
&& frogLine.IndexOf(" the frog", System.StringComparison.OrdinalIgnoreCase) < 0
&& (frogLine.IndexOf("hop", System.StringComparison.OrdinalIgnoreCase) >= 0
|| frogLine.IndexOf("leap", System.StringComparison.OrdinalIgnoreCase) >= 0
|| frogLine.IndexOf("bounc", System.StringComparison.OrdinalIgnoreCase) >= 0
|| frogLine.IndexOf("splash", System.StringComparison.OrdinalIgnoreCase) >= 0
|| frogLine.IndexOf("play", System.StringComparison.OrdinalIgnoreCase) >= 0);
bool frogJumpOk = !string.IsNullOrEmpty(frogJumpLine)
&& frogJumpLine.Length >= 16
&& frogJumpLine.IndexOf(" the frog", System.StringComparison.OrdinalIgnoreCase) < 0
&& frogJumpLine.IndexOf("young", System.StringComparison.OrdinalIgnoreCase) >= 0;
// Live ActorAsset taxonomy - not curated id maps.
ActivityContext frogCtx = ActivityContext.ForHarness("Creaker", "", "frog");
ActivityContext dogCtxLive = ActivityContext.ForHarness(actorName, "", "dog");
bool liveTaxOk = ActivityAssetCatalog.IsLiveActorAssetId("frog")
&& frogCtx.Voice.BaseBodyTag == ActivityBodyTag.Amphibian
&& frogCtx.TaxonomicClass.Equals(
"amphibia",
System.StringComparison.OrdinalIgnoreCase)
&& dogCtxLive.Family == ActivityVoiceFamilies.Canine
&& dogCtxLive.TaxonomicFamily.Equals(
"canidae",
System.StringComparison.OrdinalIgnoreCase);
// Species with no polish override still gets taxonomy manner without naming itself.
ActivityProse.Format(
ActivityKind.TaskStart,
"task_unit_play",
ActivityContext.ForHarness("Horn", "", "rhino"),
"Playing",
10,
1,
out string rhinoLine,
out _);
bool rhinoOk = !string.IsNullOrEmpty(rhinoLine)
&& rhinoLine.Length >= 16
&& rhinoLine.IndexOf(" the rhino", System.StringComparison.OrdinalIgnoreCase) < 0;
pass = dogOk && catOk && humanOk && unloadOk && distinct && placeRejectOk && placeKeepOk
&& insectRejectOk && frogOk && frogJumpOk && liveTaxOk && rhinoOk && socialBindOk
&& bonfireOk && unloadTownOk;
detail =
$"dogOk={dogOk} catOk={catOk} humanOk={humanOk} unloadOk={unloadOk} distinct={distinct} "
+ $"placeRejectOk={placeRejectOk} placeKeepOk={placeKeepOk} insectRejectOk={insectRejectOk} "
+ $"frogOk={frogOk} frogJumpOk={frogJumpOk} liveTaxOk={liveTaxOk} rhinoOk={rhinoOk} "
+ $"socialBindOk={socialBindOk} bonfireOk={bonfireOk} unloadTownOk={unloadTownOk} "
+ $"frogFamily={frogCtx.Family} frogClass={frogCtx.TaxonomicClass} "
+ $"dogFamily={dogCtxLive.Family} dogTaxFamily={dogCtxLive.TaxonomicFamily} "
+ $"dog='{dogLine}' cat='{catLine}' human='{humanLine}' unload='{unloadLine}' "
+ $"buffalo='{buffaloLine}' buffaloTown='{buffaloTownLine}' beetle='{beetleLine}' "
+ $"frog='{frogLine}' frogJump='{frogJumpLine}' rhino='{rhinoLine}' social='{socialLine}' "
+ $"bonfire='{bonfireLine}' unloadTown='{unloadTownLine}'";
break;
}
case "activity_taxonomy_audit":
{
ActivityVoiceAuditResult audit = ActivityVoiceHarness.RunAudit(HarnessDir());
pass = audit.Passed;
detail = audit.Detail;
break;
}
case "activity_status_audit":
{
ActivityStatusAuditResult audit = ActivityStatusHarness.RunAudit(HarnessDir());
pass = audit.Passed;
detail = audit.Detail;
break;
}
case "activity_happiness_audit":
{
ActivityHappinessAuditResult audit = ActivityHappinessHarness.RunAudit(HarnessDir());
pass = audit.Passed;
detail = audit.Detail;
break;
}
case "world_log_audit":
{
WorldLogEventAuditResult audit = WorldLogEventHarness.RunAudit(HarnessDir());
pass = audit.Passed;
detail = audit.Detail;
break;
}
case "disaster_war_inventory":
case "disaster_war_audit":
{
DisasterWarAuditResult audit = DisasterWarHarness.RunAudit(HarnessDir());
WorldLogEventHarness.DumpDisasterWarInventory(HarnessDir());
pass = audit.Passed;
detail = audit.Detail;
break;
}
case "domain_event_audit":
{
DomainEventAuditResult audit = DomainEventHarness.RunAudit(HarnessDir());
pass = audit.Passed;
detail = audit.Detail;
break;
}
case "mutation_discovery":
{
MutationDiscoveryResult discovery = MutationDiscoveryHarness.Run(HarnessDir());
pass = discovery.Passed;
detail = discovery.Detail;
break;
}
case "activity_happiness_count":
{
int want = ParseCountExpect(cmd, defaultValue: 1);
int got = HappinessEventRouter.NotesSinceClear;
string mode = (cmd.label ?? "exact").Trim().ToLowerInvariant();
pass = mode == "min" ? got >= want : got == want;
detail = $"notes={got} want={want} mode={mode} last={HappinessEventRouter.LastEffectId}";
break;
}
case "activity_happiness_ring_count":
{
int want = ParseCountExpect(cmd, defaultValue: 1);
int got = HappinessEventRouter.RingWritesSinceClear;
string mode = (cmd.label ?? "exact").Trim().ToLowerInvariant();
pass = mode == "min" ? got >= want : got == want;
detail = $"ring={got} want={want} mode={mode}";
break;
}
case "activity_happiness_life_count":
{
int want = ParseCountExpect(cmd, defaultValue: 1);
int got = HappinessEventRouter.LifeWritesSinceClear;
string mode = (cmd.label ?? "exact").Trim().ToLowerInvariant();
pass = mode == "min" ? got >= want : got == want;
detail = $"life={got} want={want} mode={mode}";
break;
}
case "activity_happiness_task_id":
{
long id = ResolveActivitySubjectId();
string want = cmd.value ?? "";
string got = "";
IReadOnlyList<ActivityEntry> list = ActivityLog.LatestForSubject(id, 1);
if (list != null && list.Count > 0 && list[0] != null)
{
got = list[0].TaskId ?? "";
}
pass = !string.IsNullOrEmpty(want)
&& got.Equals(want, StringComparison.OrdinalIgnoreCase);
detail = $"got='{got}' want='{want}' id={id}";
break;
}
case "activity_happiness_merged":
{
int want = ParseCountExpect(cmd, defaultValue: 1);
int got = HappinessEventRouter.MergedSinceClear;
string mode = (cmd.label ?? "min").Trim().ToLowerInvariant();
pass = mode == "exact" ? got == want : got >= want;
detail = $"merged={got} want={want} mode={mode}";
break;
}
case "activity_status_gain_count":
{
int want = ParseCountExpect(cmd, defaultValue: 1);
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<ActivityEntry> 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<string> 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<string> actors = ActivityAssetCatalog.EnumerateLiveActorAssetIds();
List<string> 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<string> 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<ChronicleEntry> 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<ChronicleCharacterSummary> 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 drops = InterestDropLog.FormatRecent(6);
string snap =
$"idle={SpectatorMode.Active} focus={MoveCamera.hasFocusUnit()} powerBar={CanvasMain.isBottomBarShowing()} unit={FocusLabel()} tip={CameraDirector.LastWatchLabel} tipAsset={CameraDirector.LastWatchAssetId} bad={StateProbe.BadEventCount} score={InterestDirector.CurrentScoreLabel} discoveryPending={SpeciesDiscovery.PendingCount} caption={WatchCaption.LastHeadline} chronicle={Chronicle.Count} drops={InterestDropLog.Count}";
if (!string.IsNullOrEmpty(drops))
{
snap += " recentDrop=[" + drops + "]";
}
_cmdOk++;
Emit(cmd, ok: true, detail: snap);
LogService.LogInfo("[IdleSpectator][SNAP] " + snap);
}
private static void DoDiscoveryNote(HarnessCommand cmd)
{
string assetId = ResolveAssetId(cmd);
if (string.IsNullOrEmpty(assetId))
{
_cmdFail++;
Emit(cmd, ok: false, detail: "missing asset for discovery_note");
return;
}
Vector3 pos = CameraPos();
Actor unit = ResolveUnit(assetId);
if (unit != null)
{
pos = unit.current_position;
}
bool ok = SpeciesDiscovery.HarnessNoteAsset(assetId, pos);
if (ok)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
Emit(cmd, ok, detail: $"asset={assetId} pending={SpeciesDiscovery.PendingCount}");
}
private static void DoWatchGhost(HarnessCommand cmd)
{
if (!WorldReady())
{
if (RetryFront(cmd, "world_not_ready"))
{
return;
}
_cmdFail++;
Emit(cmd, ok: false, detail: "world_not_ready");
return;
}
Actor near = ResolveUnit(_lastSpawnedAssetId);
if (near == null)
{
near = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 5000f);
}
if (near == null)
{
_cmdFail++;
Emit(cmd, ok: false, detail: "no unit near camera for ghost watch");
return;
}
// Claim a species that is extremely unlikely to match the focused unit.
string fakeAsset = string.IsNullOrEmpty(cmd.asset) || cmd.asset == "auto"
? "__ghost_species__"
: cmd.asset;
string label = string.IsNullOrEmpty(cmd.label)
? $"New species: {fakeAsset}"
: cmd.label.Replace("{asset}", fakeAsset);
InterestEvent interest = new InterestEvent
{
Score = 50f,
Position = near.current_position,
FollowUnit = null,
Label = label,
CreatedAt = Time.unscaledTime,
AssetId = fakeAsset
};
string before = FocusKey();
CameraDirector.Watch(interest);
string after = FocusKey();
bool unchanged = before == after && MoveCamera.hasFocusUnit();
if (unchanged)
{
_cmdOk++;
}
else
{
_cmdFail++;
}
Emit(cmd, unchanged, detail: $"ghost={fakeAsset} before={before} after={after} tip={CameraDirector.LastWatchLabel}");
}
private static string ResolveAssetId(HarnessCommand cmd)
{
string assetId = string.IsNullOrEmpty(cmd.asset) ? cmd.value : cmd.asset;
if (assetId == "auto" || string.IsNullOrEmpty(assetId))
{
assetId = _lastSpawnedAssetId;
}
return assetId ?? "";
}
private static string FocusKey()
{
if (!MoveCamera.hasFocusUnit() || MoveCamera._focus_unit == null)
{
return "-";
}
Actor unit = MoveCamera._focus_unit;
string asset = unit.asset != null ? unit.asset.id : "?";
return $"{asset}@{unit.GetHashCode()}";
}
private static void IngestNewCommands()
{
string path = CommandsPath();
if (!File.Exists(path))
{
return;
}
long length;
try
{
length = new FileInfo(path).Length;
}
catch
{
return;
}
if (length < _offset)
{
_offset = 0;
}
if (length == _offset)
{
return;
}
try
{
using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
fs.Seek(_offset, SeekOrigin.Begin);
using (StreamReader reader = new StreamReader(fs, Encoding.UTF8))
{
string line;
while ((line = reader.ReadLine()) != null)
{
_offset = fs.Position;
line = line.Trim();
if (string.IsNullOrEmpty(line) || line.StartsWith("#"))
{
continue;
}
HarnessCommand cmd = null;
try
{
cmd = JsonUtility.FromJson<HarnessCommand>(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;
}
}
/// <summary>
/// Closes Welcome Back (and optionally any ScrollWindow) so map gen / play can proceed.
/// </summary>
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<WindowWelcome>() != 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<WindowWelcome>() != 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 long ResolveActivitySubjectId()
{
// Prefer the camera focus (harness apply/assert target) over watch caption,
// which can still point at a prior spectator subject during nested regression.
try
{
if (MoveCamera.hasFocusUnit() && MoveCamera._focus_unit != null)
{
long focusId = MoveCamera._focus_unit.getID();
if (focusId != 0)
{
_activitySubjectId = focusId;
return focusId;
}
}
}
catch
{
// ignore
}
if (_activitySubjectId != 0)
{
return _activitySubjectId;
}
return WatchCaption.CurrentUnitId;
}
private static string SafeName(Actor actor)
{
try
{
string name = actor.getName();
if (!string.IsNullOrEmpty(name))
{
return name;
}
}
catch
{
// ignore
}
return actor.asset != null ? actor.asset.id : "unit";
}
/// <summary>
/// If the reason line names a living unit that is not the scene subject/related, return that name.
/// </summary>
private static string FindStrangerNamedInReason(string reason, InterestCandidate scene)
{
if (string.IsNullOrEmpty(reason) || scene == null || World.world?.units == null)
{
return "";
}
string subjectName = EventFeedUtil.SafeName(scene.FollowUnit);
string relatedName = EventFeedUtil.SafeName(scene.RelatedUnit);
try
{
foreach (Actor unit in World.world.units)
{
if (unit == null || !unit.isAlive())
{
continue;
}
if (unit == scene.FollowUnit || unit == scene.RelatedUnit)
{
continue;
}
string name = SafeName(unit);
if (string.IsNullOrEmpty(name) || name.Length < 3)
{
continue;
}
if (!string.IsNullOrEmpty(subjectName)
&& name.Equals(subjectName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (!string.IsNullOrEmpty(relatedName)
&& name.Equals(relatedName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (reason.IndexOf(name, StringComparison.OrdinalIgnoreCase) >= 0)
{
return name;
}
}
}
catch
{
// ignore
}
return "";
}
private static float EventStrengthFromTierHint(string raw, float fallback)
{
if (string.IsNullOrEmpty(raw))
{
return fallback;
}
switch (raw.Trim().ToLowerInvariant())
{
case "ambient":
return 25f;
case "curiosity":
return 40f;
case "action":
// Clears cutInMargin (35) over enriched Curiosity CharacterLed (~64).
return 100f;
case "story":
return 80f;
case "epic":
// Clears cutInMargin over Action (~108 TotalScore).
return 150f;
default:
return fallback;
}
}
private static InterestLeadKind LeadFromTierHint(string raw, InterestLeadKind fallback)
{
if (string.IsNullOrEmpty(raw))
{
return fallback;
}
string s = raw.Trim().ToLowerInvariant();
if (s == "ambient" || s == "curiosity" || s == "story")
{
return InterestLeadKind.CharacterLed;
}
if (s == "action" || s == "epic")
{
return InterestLeadKind.EventLed;
}
return fallback;
}
private static int ParseCountExpect(HarnessCommand cmd, int defaultValue)
{
// Prefer explicit value= so Step()'s default count=1 does not override want=0.
if (!string.IsNullOrEmpty(cmd.value)
&& int.TryParse(cmd.value.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out int fromValue))
{
return fromValue;
}
if (cmd.count != 1 || string.IsNullOrEmpty(cmd.value))
{
// count explicitly set via Step(..., count: N) when N != default 1, or no value given.
if (string.IsNullOrEmpty(cmd.value))
{
return cmd.count;
}
}
return defaultValue;
}
private static 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;
}
}
}
/// <summary>
/// Script drops <c>.harness/reset</c> (and/or writes offset=0) before a new command batch.
/// Without this, a same-length rewrite of commands.jsonl is skipped forever.
/// </summary>
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<HarnessCommand> rest = new List<HarnessCommand>(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");
}