Working smoke test
This commit is contained in:
parent
331a1d8583
commit
08d781a67b
8 changed files with 681 additions and 112 deletions
|
|
@ -24,6 +24,12 @@ public static class AgentHarness
|
||||||
private static int _cmdFail;
|
private static int _cmdFail;
|
||||||
private static string _lastBatchId = "";
|
private static string _lastBatchId = "";
|
||||||
private static readonly Queue<HarnessCommand> Queue = new Queue<HarnessCommand>();
|
private static readonly Queue<HarnessCommand> Queue = new Queue<HarnessCommand>();
|
||||||
|
private static Actor _lastSpawned;
|
||||||
|
private static string _lastSpawnedAssetId = "";
|
||||||
|
private static readonly string[] PreferredSpawnAssets =
|
||||||
|
{
|
||||||
|
"sheep", "cow", "pig", "chicken", "dog", "cat", "rabbit", "monkey", "fox", "crab"
|
||||||
|
};
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
private class LastResult
|
private class LastResult
|
||||||
|
|
@ -48,6 +54,7 @@ public static class AgentHarness
|
||||||
public static void Update()
|
public static void Update()
|
||||||
{
|
{
|
||||||
EnsureDirs();
|
EnsureDirs();
|
||||||
|
CheckExternalReset();
|
||||||
IngestNewCommands();
|
IngestNewCommands();
|
||||||
|
|
||||||
// Welcome Back / other ScrollWindows block world start until closed.
|
// Welcome Back / other ScrollWindows block world start until closed.
|
||||||
|
|
@ -98,9 +105,9 @@ public static class AgentHarness
|
||||||
{
|
{
|
||||||
case "wait_world":
|
case "wait_world":
|
||||||
TryDismissBlockingWindows();
|
TryDismissBlockingWindows();
|
||||||
if (!WorldReady() || IsBlockingWindowOpen())
|
if (!WorldReady() || IsBlockingWindowOpen() || !HasAnyAliveUnit())
|
||||||
{
|
{
|
||||||
Queue.Enqueue(cmd);
|
RequeueFront(cmd);
|
||||||
_waitUntil = Time.unscaledTime + 0.35f;
|
_waitUntil = Time.unscaledTime + 0.35f;
|
||||||
if (!_readyLogged)
|
if (!_readyLogged)
|
||||||
{
|
{
|
||||||
|
|
@ -136,8 +143,21 @@ public static class AgentHarness
|
||||||
case "spectator":
|
case "spectator":
|
||||||
{
|
{
|
||||||
bool on = ParseBool(cmd.value, defaultValue: true);
|
bool on = ParseBool(cmd.value, defaultValue: true);
|
||||||
|
string expect = (cmd.expect ?? "").Trim().ToLowerInvariant();
|
||||||
SpectatorMode.SetActive(on);
|
SpectatorMode.SetActive(on);
|
||||||
bool ok = SpectatorMode.Active == 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)
|
if (ok)
|
||||||
{
|
{
|
||||||
_cmdOk++;
|
_cmdOk++;
|
||||||
|
|
@ -147,7 +167,23 @@ public static class AgentHarness
|
||||||
_cmdFail++;
|
_cmdFail++;
|
||||||
}
|
}
|
||||||
|
|
||||||
Emit(cmd, ok, detail: $"idle={SpectatorMode.Active}");
|
Emit(cmd, ok, detail);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "simulate_input":
|
||||||
|
{
|
||||||
|
bool exited = InterestDirector.SimulateManualInputExit();
|
||||||
|
if (exited)
|
||||||
|
{
|
||||||
|
_cmdOk++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_cmdFail++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Emit(cmd, exited, detail: $"idle={SpectatorMode.Active}");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -155,6 +191,10 @@ public static class AgentHarness
|
||||||
DoSpawn(cmd);
|
DoSpawn(cmd);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "pick_unit":
|
||||||
|
DoPickUnit(cmd);
|
||||||
|
break;
|
||||||
|
|
||||||
case "focus":
|
case "focus":
|
||||||
DoFocus(cmd);
|
DoFocus(cmd);
|
||||||
break;
|
break;
|
||||||
|
|
@ -245,21 +285,74 @@ public static class AgentHarness
|
||||||
LogService.LogInfo($"[IdleSpectator][HARNESS] scenario={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)
|
private static void DoSpawn(HarnessCommand cmd)
|
||||||
{
|
{
|
||||||
if (!WorldReady())
|
if (!WorldReady())
|
||||||
{
|
{
|
||||||
|
if (RetryFront(cmd, "world_not_ready"))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_cmdFail++;
|
_cmdFail++;
|
||||||
Emit(cmd, ok: false, detail: "world_not_ready");
|
Emit(cmd, ok: false, detail: "world_not_ready");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string assetId = string.IsNullOrEmpty(cmd.asset) ? cmd.value : cmd.asset;
|
string assetId = string.IsNullOrEmpty(cmd.asset) ? cmd.value : cmd.asset;
|
||||||
if (string.IsNullOrEmpty(assetId))
|
if (string.IsNullOrEmpty(assetId) || assetId == "auto")
|
||||||
{
|
{
|
||||||
_cmdFail++;
|
assetId = PickSpawnAssetId();
|
||||||
Emit(cmd, ok: false, detail: "missing asset");
|
if (string.IsNullOrEmpty(assetId))
|
||||||
return;
|
{
|
||||||
|
_cmdFail++;
|
||||||
|
Emit(cmd, ok: false, detail: "no spawnable asset");
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (AssetManager.actor_library?.get(assetId) == null)
|
if (AssetManager.actor_library?.get(assetId) == null)
|
||||||
|
|
@ -270,8 +363,26 @@ public static class AgentHarness
|
||||||
}
|
}
|
||||||
|
|
||||||
WorldTile tile = TileNearCamera();
|
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 (tile == null)
|
||||||
{
|
{
|
||||||
|
if (RetryFront(cmd, "no_tile"))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_cmdFail++;
|
_cmdFail++;
|
||||||
Emit(cmd, ok: false, detail: "no tile near camera");
|
Emit(cmd, ok: false, detail: "no tile near camera");
|
||||||
return;
|
return;
|
||||||
|
|
@ -283,54 +394,61 @@ public static class AgentHarness
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
Actor actor = World.world.units.spawnNewUnit(assetId, tile, pSpawnSound: false, pMiracleSpawn: true);
|
Actor actor = World.world.units.spawnNewUnit(assetId, tile, pSpawnSound: false, pMiracleSpawn: true);
|
||||||
if (actor != null)
|
if (actor != null && actor.isAlive())
|
||||||
{
|
{
|
||||||
spawned++;
|
spawned++;
|
||||||
last = actor;
|
last = actor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ok = spawned > 0;
|
if (spawned <= 0 || last == null || !last.isAlive())
|
||||||
if (ok)
|
|
||||||
{
|
|
||||||
_cmdOk++;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
|
if (RetryFront(cmd, "spawn_zero"))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_cmdFail++;
|
_cmdFail++;
|
||||||
|
Emit(cmd, ok: false, detail: $"spawned=0/{count} asset={assetId}");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string who = last != null ? SafeName(last) : "-";
|
_lastSpawned = last;
|
||||||
Emit(cmd, ok, detail: $"spawned={spawned}/{count} asset={assetId} last={who}");
|
_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)
|
private static void DoFocus(HarnessCommand cmd)
|
||||||
{
|
{
|
||||||
if (!WorldReady())
|
if (!WorldReady())
|
||||||
{
|
{
|
||||||
|
if (RetryFront(cmd, "world_not_ready"))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_cmdFail++;
|
_cmdFail++;
|
||||||
Emit(cmd, ok: false, detail: "world_not_ready");
|
Emit(cmd, ok: false, detail: "world_not_ready");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Actor unit = null;
|
|
||||||
string assetId = string.IsNullOrEmpty(cmd.asset) ? cmd.value : cmd.asset;
|
string assetId = string.IsNullOrEmpty(cmd.asset) ? cmd.value : cmd.asset;
|
||||||
if (!string.IsNullOrEmpty(assetId) && assetId != "nearest")
|
if (assetId == "nearest" || assetId == "auto")
|
||||||
{
|
{
|
||||||
Vector3 pos = CameraPos();
|
assetId = assetId == "auto" ? _lastSpawnedAssetId : null;
|
||||||
unit = WorldActivityScanner.FindNearestAliveUnit(pos, 200f, assetId);
|
|
||||||
}
|
|
||||||
else if (MoveCamera.hasFocusUnit())
|
|
||||||
{
|
|
||||||
unit = MoveCamera._focus_unit;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
unit = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Actor unit = ResolveUnit(assetId);
|
||||||
if (unit == null || !unit.isAlive())
|
if (unit == null || !unit.isAlive())
|
||||||
{
|
{
|
||||||
|
if (RetryFront(cmd, "no_unit"))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_cmdFail++;
|
_cmdFail++;
|
||||||
Emit(cmd, ok: false, detail: "no unit to focus");
|
Emit(cmd, ok: false, detail: "no unit to focus");
|
||||||
return;
|
return;
|
||||||
|
|
@ -345,34 +463,44 @@ public static class AgentHarness
|
||||||
{
|
{
|
||||||
if (!WorldReady())
|
if (!WorldReady())
|
||||||
{
|
{
|
||||||
|
if (RetryFront(cmd, "world_not_ready"))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_cmdFail++;
|
_cmdFail++;
|
||||||
Emit(cmd, ok: false, detail: "world_not_ready");
|
Emit(cmd, ok: false, detail: "world_not_ready");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string assetId = cmd.asset;
|
string assetId = cmd.asset;
|
||||||
Actor follow = null;
|
if (assetId == "auto")
|
||||||
if (!string.IsNullOrEmpty(assetId))
|
|
||||||
{
|
{
|
||||||
follow = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f, assetId);
|
assetId = _lastSpawnedAssetId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Actor follow = ResolveUnit(string.IsNullOrEmpty(assetId) ? null : assetId);
|
||||||
if (follow == null)
|
if (follow == null)
|
||||||
{
|
{
|
||||||
follow = WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 200f);
|
if (RetryFront(cmd, string.IsNullOrEmpty(assetId) ? "no_unit" : "no_asset_unit"))
|
||||||
}
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (follow == null)
|
|
||||||
{
|
|
||||||
_cmdFail++;
|
_cmdFail++;
|
||||||
Emit(cmd, ok: false, detail: "no unit for watch");
|
Emit(cmd, ok: false, detail: string.IsNullOrEmpty(assetId) ? "no unit for watch" : "no unit for asset: " + assetId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
InterestTier tier = ParseTier(cmd.tier, InterestTier.Curiosity);
|
InterestTier tier = ParseTier(cmd.tier, InterestTier.Curiosity);
|
||||||
|
string resolvedAsset = follow.asset != null ? follow.asset.id : assetId;
|
||||||
string label = string.IsNullOrEmpty(cmd.label)
|
string label = string.IsNullOrEmpty(cmd.label)
|
||||||
? $"Harness: {(follow.asset != null ? follow.asset.id : SafeName(follow))}"
|
? $"Harness: {resolvedAsset}"
|
||||||
: cmd.label;
|
: cmd.label.Replace("{asset}", resolvedAsset ?? "");
|
||||||
|
if (label.Contains("{asset}"))
|
||||||
|
{
|
||||||
|
label = label.Replace("{asset}", resolvedAsset ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
InterestEvent interest = new InterestEvent
|
InterestEvent interest = new InterestEvent
|
||||||
{
|
{
|
||||||
|
|
@ -522,16 +650,32 @@ public static class AgentHarness
|
||||||
string tipAsset = CameraDirector.LastWatchAssetId ?? "";
|
string tipAsset = CameraDirector.LastWatchAssetId ?? "";
|
||||||
Actor unit = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
Actor unit = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||||
string unitAsset = unit?.asset != null ? unit.asset.id : "";
|
string unitAsset = unit?.asset != null ? unit.asset.id : "";
|
||||||
pass = !string.IsNullOrEmpty(tipAsset)
|
string tip = CameraDirector.LastWatchLabel ?? "";
|
||||||
&& tipAsset == unitAsset
|
bool assetsMatch = !string.IsNullOrEmpty(tipAsset)
|
||||||
&& MoveCamera.hasFocusUnit();
|
&& 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 =
|
detail =
|
||||||
$"tipAsset={tipAsset} unitAsset={unitAsset} tip={CameraDirector.LastWatchLabel}";
|
$"tipAsset={tipAsset} unitAsset={unitAsset} tip={tip}";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "unit_asset":
|
case "unit_asset":
|
||||||
{
|
{
|
||||||
string wantAsset = string.IsNullOrEmpty(cmd.asset) ? cmd.value : cmd.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;
|
Actor unit = MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null;
|
||||||
string unitAsset = unit?.asset != null ? unit.asset.id : "";
|
string unitAsset = unit?.asset != null ? unit.asset.id : "";
|
||||||
pass = !string.IsNullOrEmpty(wantAsset) && wantAsset == unitAsset;
|
pass = !string.IsNullOrEmpty(wantAsset) && wantAsset == unitAsset;
|
||||||
|
|
@ -544,6 +688,29 @@ public static class AgentHarness
|
||||||
detail = $"bad={StateProbe.BadEventCount}";
|
detail = $"bad={StateProbe.BadEventCount}";
|
||||||
break;
|
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;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
pass = false;
|
pass = false;
|
||||||
detail = "unknown expect: " + expect;
|
detail = "unknown expect: " + expect;
|
||||||
|
|
@ -705,6 +872,11 @@ public static class AgentHarness
|
||||||
return Config.game_loaded && World.world != null && !SmoothLoader.isLoading();
|
return Config.game_loaded && World.world != null && !SmoothLoader.isLoading();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool HasAnyAliveUnit()
|
||||||
|
{
|
||||||
|
return WorldActivityScanner.FindNearestAliveUnit(CameraPos(), 5000f) != null;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool ShouldAutoDismissUi()
|
private static bool ShouldAutoDismissUi()
|
||||||
{
|
{
|
||||||
if (Queue.Count > 0 || _waitUntil > 0f)
|
if (Queue.Count > 0 || _waitUntil > 0f)
|
||||||
|
|
@ -915,6 +1087,143 @@ public static class AgentHarness
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
_readyLogged = false;
|
||||||
|
_offset = 0;
|
||||||
|
_lastSpawned = null;
|
||||||
|
_lastSpawnedAssetId = "";
|
||||||
|
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 string HarnessDir()
|
private static string HarnessDir()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(ModClass.ModFolder))
|
if (string.IsNullOrEmpty(ModClass.ModFolder))
|
||||||
|
|
|
||||||
|
|
@ -17,4 +17,6 @@ internal class HarnessCommand
|
||||||
public string expect = "";
|
public string expect = "";
|
||||||
public int count = 1;
|
public int count = 1;
|
||||||
public float wait;
|
public float wait;
|
||||||
|
/// <summary>Transient retry counter (not written by the shell runner).</summary>
|
||||||
|
public int retries;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ internal static class HarnessScenarios
|
||||||
return Smoke();
|
return Smoke();
|
||||||
case "tip_match":
|
case "tip_match":
|
||||||
return TipMatch();
|
return TipMatch();
|
||||||
|
case "critical":
|
||||||
|
case "critical_smoke":
|
||||||
|
return CriticalSmoke();
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -27,12 +30,14 @@ internal static class HarnessScenarios
|
||||||
Step("s0", "dismiss_windows"),
|
Step("s0", "dismiss_windows"),
|
||||||
Step("s1", "wait_world"),
|
Step("s1", "wait_world"),
|
||||||
Step("s2", "reset_counters"),
|
Step("s2", "reset_counters"),
|
||||||
Step("s3", "spectator", value: "off"),
|
Step("s3", "pick_unit", asset: "auto"),
|
||||||
Step("s4", "assert", expect: "idle", value: "false"),
|
Step("s4", "spectator", value: "off"),
|
||||||
Step("s5", "spectator", value: "on"),
|
Step("s5", "assert", expect: "idle", value: "false"),
|
||||||
Step("s6", "wait", wait: 0.75f),
|
Step("s6", "spectator", value: "on"),
|
||||||
Step("s7", "assert", expect: "health"),
|
Step("s7", "focus", asset: "auto"),
|
||||||
Step("s8", "snapshot"),
|
Step("s8", "wait", wait: 0.35f),
|
||||||
|
Step("s9", "assert", expect: "health"),
|
||||||
|
Step("s10", "snapshot"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,15 +48,73 @@ internal static class HarnessScenarios
|
||||||
Step("t0", "dismiss_windows"),
|
Step("t0", "dismiss_windows"),
|
||||||
Step("t1", "wait_world"),
|
Step("t1", "wait_world"),
|
||||||
Step("t2", "reset_counters"),
|
Step("t2", "reset_counters"),
|
||||||
Step("t3", "spectator", value: "on"),
|
Step("t3", "pick_unit", asset: "auto"),
|
||||||
Step("t4", "spawn", asset: "crab", count: 2),
|
Step("t4", "spectator", value: "on"),
|
||||||
Step("t5", "wait", wait: 0.5f),
|
Step("t5", "watch", asset: "auto", label: "New species: {asset}", tier: "Curiosity"),
|
||||||
Step("t6", "watch", asset: "crab", label: "New species: crab", tier: "Curiosity"),
|
Step("t6", "wait", wait: 0.3f),
|
||||||
Step("t7", "wait", wait: 0.35f),
|
Step("t7", "assert", expect: "tip_matches_unit"),
|
||||||
Step("t8", "assert", expect: "tip_matches_unit"),
|
Step("t8", "assert", expect: "unit_asset", asset: "auto"),
|
||||||
Step("t9", "assert", expect: "unit_asset", asset: "crab"),
|
Step("t9", "assert", expect: "power_bar", value: "false"),
|
||||||
Step("t10", "assert", expect: "power_bar", value: "false"),
|
Step("t10", "snapshot"),
|
||||||
Step("t11", "snapshot"),
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Critical E2E against already-living map units (no fragile miracle spawns).
|
||||||
|
/// </summary>
|
||||||
|
private static List<HarnessCommand> CriticalSmoke()
|
||||||
|
{
|
||||||
|
return new List<HarnessCommand>
|
||||||
|
{
|
||||||
|
Step("c00", "dismiss_windows"),
|
||||||
|
Step("c01", "wait_world"),
|
||||||
|
Step("c02", "reset_counters"),
|
||||||
|
|
||||||
|
// Lock onto a living map unit for the rest of the scenario.
|
||||||
|
Step("c05", "pick_unit", asset: "auto"),
|
||||||
|
Step("c06", "wait", wait: 0.25f),
|
||||||
|
|
||||||
|
Step("c10", "set_setting", expect: "enabled", value: "false"),
|
||||||
|
Step("c11", "assert", expect: "enabled", value: "false"),
|
||||||
|
Step("c12", "spectator", value: "on", expect: "blocked"),
|
||||||
|
Step("c13", "assert", expect: "idle", value: "false"),
|
||||||
|
Step("c14", "set_setting", expect: "enabled", value: "true"),
|
||||||
|
Step("c15", "assert", expect: "enabled", value: "true"),
|
||||||
|
|
||||||
|
Step("c20", "spectator", value: "off"),
|
||||||
|
Step("c21", "assert", expect: "idle", value: "false"),
|
||||||
|
Step("c22", "spectator", value: "on"),
|
||||||
|
Step("c23", "focus", asset: "auto"),
|
||||||
|
Step("c24", "reset_counters"),
|
||||||
|
Step("c25", "wait", wait: 0.35f),
|
||||||
|
Step("c26", "assert", expect: "health"),
|
||||||
|
Step("c27", "assert", expect: "power_bar", value: "false"),
|
||||||
|
Step("c28", "assert", expect: "no_bad"),
|
||||||
|
|
||||||
|
Step("c30", "watch", asset: "auto", label: "New species: {asset}", tier: "Curiosity"),
|
||||||
|
Step("c31", "wait", wait: 0.3f),
|
||||||
|
Step("c32", "assert", expect: "tip_matches_unit"),
|
||||||
|
Step("c33", "assert", expect: "unit_asset", asset: "auto"),
|
||||||
|
Step("c34", "assert", expect: "tip_contains", value: "auto"),
|
||||||
|
Step("c35", "assert", expect: "power_bar", value: "false"),
|
||||||
|
Step("c36", "assert", expect: "no_bad"),
|
||||||
|
|
||||||
|
Step("c40", "watch", asset: "auto", label: "Retarget A: {asset}", tier: "Curiosity"),
|
||||||
|
Step("c41", "wait", wait: 0.25f),
|
||||||
|
Step("c42", "focus", asset: "auto"),
|
||||||
|
Step("c43", "wait", wait: 0.25f),
|
||||||
|
Step("c44", "watch", asset: "auto", label: "Retarget B: {asset}", tier: "Curiosity"),
|
||||||
|
Step("c45", "wait", wait: 0.35f),
|
||||||
|
Step("c46", "assert", expect: "health"),
|
||||||
|
Step("c47", "assert", expect: "no_bad"),
|
||||||
|
Step("c48", "assert", expect: "power_bar", value: "false"),
|
||||||
|
|
||||||
|
Step("c50", "simulate_input"),
|
||||||
|
Step("c51", "wait", wait: 0.25f),
|
||||||
|
Step("c52", "assert", expect: "idle", value: "false"),
|
||||||
|
Step("c53", "assert", expect: "no_bad"),
|
||||||
|
|
||||||
|
Step("c99", "snapshot"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,13 +53,17 @@ public static class InterestDirector
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// During harness batches, do not ambient-steal focus. The harness picks units explicitly.
|
||||||
|
if (AgentHarness.Busy)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Harness drives the camera/assert loop; ignore manual-input exit while commands run.
|
// Harness drives the camera/assert loop; ignore manual-input exit while commands run.
|
||||||
if (!AgentHarness.Busy
|
if (Time.unscaledTime - _enabledAt >= InputExitGraceSeconds
|
||||||
&& Time.unscaledTime - _enabledAt >= InputExitGraceSeconds
|
|
||||||
&& PlayerTookManualControl())
|
&& PlayerTookManualControl())
|
||||||
{
|
{
|
||||||
SpectatorMode.SetActive(false);
|
ExitFromManualInput();
|
||||||
WorldTip.showNowTop("Idle Spectator paused (manual input)", pTranslate: false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,6 +143,36 @@ public static class InterestDirector
|
||||||
sinceSwitch);
|
sinceSwitch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Harness: pulse camera-drag input and take the real manual-exit path.
|
||||||
|
/// </summary>
|
||||||
|
public static bool SimulateManualInputExit()
|
||||||
|
{
|
||||||
|
if (!SpectatorMode.Active)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
MoveCamera.camera_drag_run = true;
|
||||||
|
bool detected = PlayerTookManualControl();
|
||||||
|
if (!detected)
|
||||||
|
{
|
||||||
|
MoveCamera.camera_drag_run = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExitFromManualInput();
|
||||||
|
MoveCamera.camera_drag_run = false;
|
||||||
|
return !SpectatorMode.Active;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ExitFromManualInput()
|
||||||
|
{
|
||||||
|
SpectatorMode.SetActive(false);
|
||||||
|
WorldTip.showNowTop("Idle Spectator paused (manual input)", pTranslate: false);
|
||||||
|
LogService.LogInfo("[IdleSpectator] Spectator paused (manual input)");
|
||||||
|
}
|
||||||
|
|
||||||
private static bool PlayerTookManualControl()
|
private static bool PlayerTookManualControl()
|
||||||
{
|
{
|
||||||
// Rising-edge style checks only - avoid sticky flags like already_used_power
|
// Rising-edge style checks only - avoid sticky flags like already_used_power
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,10 @@ public class ModClass : MonoBehaviour, IMod, IConfigurable, ILocalizable
|
||||||
AgentHarness.Update();
|
AgentHarness.Update();
|
||||||
SpectatorMode.PollInput();
|
SpectatorMode.PollInput();
|
||||||
InterestDirector.Update();
|
InterestDirector.Update();
|
||||||
SpeciesDiscovery.Update();
|
if (!AgentHarness.Busy)
|
||||||
|
{
|
||||||
|
SpeciesDiscovery.Update();
|
||||||
|
}
|
||||||
StateProbe.Update();
|
StateProbe.Update();
|
||||||
AutoTest.Update();
|
AutoTest.Update();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -406,6 +406,16 @@ public static class WorldActivityScanner
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Actor FindNearestAliveUnit(Vector3 pos, float maxDist, string requireAssetId = null)
|
public static Actor FindNearestAliveUnit(Vector3 pos, float maxDist, string requireAssetId = null)
|
||||||
{
|
{
|
||||||
|
// Freshly spawned units are often on asset.units before units_only_alive updates.
|
||||||
|
if (!string.IsNullOrEmpty(requireAssetId))
|
||||||
|
{
|
||||||
|
Actor fromAsset = FindAliveOnAssetList(requireAssetId, pos, maxDist);
|
||||||
|
if (fromAsset != null)
|
||||||
|
{
|
||||||
|
return fromAsset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Actor best = null;
|
Actor best = null;
|
||||||
float bestDist = maxDist * maxDist;
|
float bestDist = maxDist * maxDist;
|
||||||
foreach (Actor actor in EnumerateAliveUnits())
|
foreach (Actor actor in EnumerateAliveUnits())
|
||||||
|
|
@ -429,6 +439,49 @@ public static class WorldActivityScanner
|
||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Any living unit of this asset id (asset.units first, then world scan).
|
||||||
|
/// </summary>
|
||||||
|
public static Actor FindAliveByAssetId(string assetId, Vector3 nearPos, float maxDist = 5000f)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(assetId))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FindNearestAliveUnit(nearPos, maxDist, assetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Actor FindAliveOnAssetList(string assetId, Vector3 nearPos, float maxDist)
|
||||||
|
{
|
||||||
|
ActorAsset asset = AssetManager.actor_library?.get(assetId);
|
||||||
|
if (asset?.units == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Actor best = null;
|
||||||
|
float bestDist = maxDist * maxDist;
|
||||||
|
foreach (Actor unit in asset.units)
|
||||||
|
{
|
||||||
|
if (unit == null || !unit.isAlive())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
float dx = unit.current_position.x - nearPos.x;
|
||||||
|
float dy = unit.current_position.y - nearPos.y;
|
||||||
|
float d2 = dx * dx + dy * dy;
|
||||||
|
if (d2 < bestDist)
|
||||||
|
{
|
||||||
|
bestDist = d2;
|
||||||
|
best = unit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
private static Actor FindMatchingSpeciesUnit(ActorAsset asset, Subspecies subspecies, Vector3 nearPos, float maxDist)
|
private static Actor FindMatchingSpeciesUnit(ActorAsset asset, Subspecies subspecies, Vector3 nearPos, float maxDist)
|
||||||
{
|
{
|
||||||
if (asset?.units != null)
|
if (asset?.units != null)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "IdleSpectator",
|
"name": "IdleSpectator",
|
||||||
"author": "dazed",
|
"author": "dazed",
|
||||||
"version": "0.4.1",
|
"version": "0.5.0",
|
||||||
"description": "AFK Idle Spectator (I): follows wars, disasters, battles, creatures, and new species. Includes .harness agent playtest bus.",
|
"description": "AFK Idle Spectator (I) with agent harness. Critical E2E: settings gate, focus health, tip match, retarget, manual exit.",
|
||||||
"GUID": "com.dazed.idlespectator"
|
"GUID": "com.dazed.idlespectator"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,15 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Run an IdleSpectator agent harness scenario (or raw JSONL commands).
|
# Run an IdleSpectator agent harness scenario (or raw JSONL commands).
|
||||||
|
# Starts WorldBox via Steam when it is not already running.
|
||||||
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./scripts/harness-run.sh smoke
|
# ./scripts/harness-run.sh # default: critical_smoke
|
||||||
|
# ./scripts/harness-run.sh critical_smoke
|
||||||
|
# ./scripts/harness-run.sh --repeat 3 # run scenario 3 times (all must PASS)
|
||||||
# ./scripts/harness-run.sh tip_match
|
# ./scripts/harness-run.sh tip_match
|
||||||
|
# ./scripts/harness-run.sh smoke
|
||||||
# ./scripts/harness-run.sh --cmd '{"id":"1","action":"snapshot"}'
|
# ./scripts/harness-run.sh --cmd '{"id":"1","action":"snapshot"}'
|
||||||
|
# ./scripts/harness-run.sh --no-launch smoke
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
|
@ -11,8 +17,10 @@ MOD="${IDLE_SPECTATOR_MOD:-$ROOT/IdleSpectator}"
|
||||||
HARNESS="$MOD/.harness"
|
HARNESS="$MOD/.harness"
|
||||||
LOG="${WORLDBOX_LOG:-$HOME/.config/unity3d/mkarpenko/WorldBox/Player.log}"
|
LOG="${WORLDBOX_LOG:-$HOME/.config/unity3d/mkarpenko/WorldBox/Player.log}"
|
||||||
TIMEOUT_SEC="${HARNESS_TIMEOUT:-90}"
|
TIMEOUT_SEC="${HARNESS_TIMEOUT:-90}"
|
||||||
|
STEAM_APP_ID="${WORLDBOX_APP_ID:-1206560}"
|
||||||
mkdir -p "$HARNESS"
|
BOOT_TIMEOUT_SEC="${HARNESS_BOOT_TIMEOUT:-180}"
|
||||||
|
DO_LAUNCH=1
|
||||||
|
REPEAT=1
|
||||||
|
|
||||||
scenario=""
|
scenario=""
|
||||||
raw_cmd=""
|
raw_cmd=""
|
||||||
|
|
@ -26,6 +34,14 @@ while [[ $# -gt 0 ]]; do
|
||||||
TIMEOUT_SEC="$2"
|
TIMEOUT_SEC="$2"
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
--repeat)
|
||||||
|
REPEAT="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--no-launch)
|
||||||
|
DO_LAUNCH=0
|
||||||
|
shift
|
||||||
|
;;
|
||||||
-*)
|
-*)
|
||||||
echo "Unknown flag: $1" >&2
|
echo "Unknown flag: $1" >&2
|
||||||
exit 2
|
exit 2
|
||||||
|
|
@ -38,53 +54,142 @@ while [[ $# -gt 0 ]]; do
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ -z "$scenario" && -z "$raw_cmd" ]]; then
|
if [[ -z "$scenario" && -z "$raw_cmd" ]]; then
|
||||||
scenario="smoke"
|
scenario="critical_smoke"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
rm -f "$HARNESS/last-result.json"
|
worldbox_running() {
|
||||||
: > "$HARNESS/results.jsonl"
|
local pid cmd
|
||||||
# While this file exists, harness auto-closes Welcome Back / blocking UI.
|
for pid in $(pgrep -a worldbox 2>/dev/null | awk '{print $1}'); do
|
||||||
touch "$HARNESS/auto-dismiss"
|
cmd=$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null || true)
|
||||||
|
if [[ "$cmd" == *"/common/worldbox/worldbox"* && "$cmd" != *zsh* && "$cmd" != *snap=* && "$cmd" != *harness-run* ]]; then
|
||||||
{
|
return 0
|
||||||
echo '{"id":"batch","action":"reset_counters"}'
|
|
||||||
echo '{"id":"dismiss","action":"dismiss_windows"}'
|
|
||||||
if [[ -n "$raw_cmd" ]]; then
|
|
||||||
echo "$raw_cmd"
|
|
||||||
else
|
|
||||||
echo "{\"id\":\"run\",\"action\":\"scenario\",\"value\":\"$scenario\"}"
|
|
||||||
fi
|
|
||||||
} > "$HARNESS/commands.jsonl"
|
|
||||||
|
|
||||||
echo "Harness queued: ${scenario:-raw} (mod=$MOD)"
|
|
||||||
echo "Waiting up to ${TIMEOUT_SEC}s for last-result.json ..."
|
|
||||||
|
|
||||||
deadline=$((SECONDS + TIMEOUT_SEC))
|
|
||||||
while (( SECONDS < deadline )); do
|
|
||||||
if [[ -f "$HARNESS/last-result.json" ]]; then
|
|
||||||
# NOTE: do not name this `status` - zsh treats that as read-only.
|
|
||||||
result_status="$(python3 -c "import json;print(json.load(open('$HARNESS/last-result.json')).get('status',''))" 2>/dev/null || true)"
|
|
||||||
if [[ "$result_status" == "PASS" || "$result_status" == "FAIL" || "$result_status" == "error" ]]; then
|
|
||||||
echo "===== last-result.json ====="
|
|
||||||
cat "$HARNESS/last-result.json"
|
|
||||||
echo "===== recent harness log ====="
|
|
||||||
if [[ -f "$LOG" ]]; then
|
|
||||||
rg -n "\[IdleSpectator\]\[(HARNESS|ASSERT|SNAP)\]|dismissed window" "$LOG" | tail -n 40 || true
|
|
||||||
fi
|
|
||||||
if [[ "$result_status" == "PASS" ]]; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
mod_ready() {
|
||||||
|
[[ -f "$LOG" ]] || return 1
|
||||||
|
if tail -n 200 "$LOG" 2>/dev/null | rg -q "error CS"; then
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
tail -n 200 "$LOG" 2>/dev/null | rg -q "\[IdleSpectator\]: Harmony patches applied"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_worldbox() {
|
||||||
|
if worldbox_running; then
|
||||||
|
echo "WorldBox already running"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( DO_LAUNCH == 0 )); then
|
||||||
|
echo "WorldBox is not running (pass without --no-launch to auto-start)" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Starting WorldBox (steam app $STEAM_APP_ID) ..."
|
||||||
|
echo "===== HARNESS LAUNCH $(date -Iseconds) =====" >> "$LOG" 2>/dev/null || true
|
||||||
|
nohup steam -applaunch "$STEAM_APP_ID" >/tmp/idle-spectator-harness-launch.log 2>&1 &
|
||||||
|
disown || true
|
||||||
|
|
||||||
|
local deadline=$((SECONDS + BOOT_TIMEOUT_SEC))
|
||||||
|
while (( SECONDS < deadline )); do
|
||||||
|
if mod_ready; then
|
||||||
|
echo "WorldBox + IdleSpectator ready"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ -f "$LOG" ]] && tail -n 200 "$LOG" 2>/dev/null | rg -q "error CS"; then
|
||||||
|
echo "IdleSpectator compile failed:" >&2
|
||||||
|
rg -n "error CS" "$LOG" | tail -n 40 >&2 || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if worldbox_running; then
|
||||||
|
printf '.'
|
||||||
|
else
|
||||||
|
printf 'o'
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo >&2
|
||||||
|
echo "Timed out waiting for WorldBox/IdleSpectator boot (${BOOT_TIMEOUT_SEC}s)" >&2
|
||||||
|
tail -n 40 "$LOG" 2>/dev/null >&2 || true
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
queue_and_wait() {
|
||||||
|
local run_label="$1"
|
||||||
|
rm -f "$HARNESS/last-result.json"
|
||||||
|
: > "$HARNESS/results.jsonl"
|
||||||
|
echo 0 > "$HARNESS/offset"
|
||||||
|
: > "$HARNESS/commands.jsonl"
|
||||||
|
touch "$HARNESS/reset"
|
||||||
|
touch "$HARNESS/auto-dismiss"
|
||||||
|
sleep 0.35
|
||||||
|
|
||||||
|
{
|
||||||
|
echo '{"id":"batch","action":"reset_counters"}'
|
||||||
|
echo '{"id":"dismiss","action":"dismiss_windows"}'
|
||||||
|
if [[ -n "$raw_cmd" ]]; then
|
||||||
|
echo "$raw_cmd"
|
||||||
|
else
|
||||||
|
echo "{\"id\":\"run\",\"action\":\"scenario\",\"value\":\"$scenario\"}"
|
||||||
|
fi
|
||||||
|
} > "$HARNESS/commands.jsonl"
|
||||||
|
|
||||||
|
echo "Harness queued: ${scenario:-raw} ($run_label) (mod=$MOD)"
|
||||||
|
echo "Waiting up to ${TIMEOUT_SEC}s for last-result.json (load a world if at the menu) ..."
|
||||||
|
|
||||||
|
local deadline=$((SECONDS + TIMEOUT_SEC))
|
||||||
|
while (( SECONDS < deadline )); do
|
||||||
|
if [[ -f "$HARNESS/last-result.json" ]]; then
|
||||||
|
# NOTE: do not name this `status` - zsh treats that as read-only.
|
||||||
|
local result_status
|
||||||
|
result_status="$(python3 -c "import json;print(json.load(open('$HARNESS/last-result.json')).get('status',''))" 2>/dev/null || true)"
|
||||||
|
if [[ "$result_status" == "PASS" || "$result_status" == "FAIL" || "$result_status" == "error" ]]; then
|
||||||
|
echo "===== last-result.json ($run_label) ====="
|
||||||
|
cat "$HARNESS/last-result.json"
|
||||||
|
echo "===== recent harness log ====="
|
||||||
|
if [[ -f "$LOG" ]]; then
|
||||||
|
rg -n "\[IdleSpectator\]\[(HARNESS|ASSERT|SNAP)\]|dismissed window|retry id=" "$LOG" | tail -n 50 || true
|
||||||
|
fi
|
||||||
|
if [[ "$result_status" == "PASS" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
sleep 0.5
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Timed out after ${TIMEOUT_SEC}s ($run_label)" >&2
|
||||||
|
if [[ -f "$HARNESS/last-result.json" ]]; then
|
||||||
|
cat "$HARNESS/last-result.json" >&2 || true
|
||||||
|
fi
|
||||||
|
if [[ -f "$LOG" ]]; then
|
||||||
|
rg -n "IdleSpectator|error CS|Exception" "$LOG" | tail -n 50 >&2 || true
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdir -p "$HARNESS"
|
||||||
|
ensure_worldbox
|
||||||
|
|
||||||
|
if (( TIMEOUT_SEC < 120 )); then
|
||||||
|
TIMEOUT_SEC=120
|
||||||
|
fi
|
||||||
|
|
||||||
|
pass_count=0
|
||||||
|
for ((i=1; i<=REPEAT; i++)); do
|
||||||
|
echo "======== RUN $i / $REPEAT ========"
|
||||||
|
if queue_and_wait "run $i/$REPEAT"; then
|
||||||
|
pass_count=$((pass_count + 1))
|
||||||
|
echo "RUN $i PASS"
|
||||||
|
else
|
||||||
|
echo "RUN $i FAIL" >&2
|
||||||
|
echo "Summary: $pass_count / $REPEAT passed before failure" >&2
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
sleep 0.5
|
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Timed out after ${TIMEOUT_SEC}s" >&2
|
echo "Summary: $pass_count / $REPEAT PASS"
|
||||||
if [[ -f "$HARNESS/last-result.json" ]]; then
|
exit 0
|
||||||
cat "$HARNESS/last-result.json" >&2 || true
|
|
||||||
fi
|
|
||||||
if [[ -f "$LOG" ]]; then
|
|
||||||
rg -n "IdleSpectator|error CS|Exception" "$LOG" | tail -n 50 >&2 || true
|
|
||||||
fi
|
|
||||||
exit 1
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue