arrows for idle mode

This commit is contained in:
DazedAnon 2026-07-14 18:35:06 -05:00
parent 9ae7f7ffa1
commit 9fdce47810
9 changed files with 122 additions and 2 deletions

View file

@ -152,6 +152,39 @@ Use `fast_timing` / `director_run` instead.
- Prefer `./scripts/harness-run.sh`; avoid `Application.Quit`.
- After code changes, restart/relaunch so NML recompiles.
## Launching WorldBox (required for agents)
Steam **refuses** to start when the Shell tool runs sandboxed (looks like root):
`Error: Cannot run as root user` / game never opens.
**Always** launch or restart with unrestricted permissions:
```bash
# Shell tool: required_permissions: ["all"]
cd /home/dazed/Projects/worldbox-mods/idle-mode
./scripts/harness-run.sh --repeat 3 critical_smoke
```
That runner starts Steam via `steam -applaunch 1206560` when needed.
### Manual restart after a mod.json bump
```bash
# Shell tool: required_permissions: ["all"]
# Kill only the game binary (not Steam itself):
kill $(pgrep -f '/Steam/steamapps/common/worldbox/worldbox$') 2>/dev/null
# or: kill -9 <pid of .../common/worldbox/worldbox>
nohup steam -applaunch 1206560 >/tmp/idle-spectator-harness-launch.log 2>&1 &
# Wait until NML loaded the mod (not just process up):
# ~/.config/unity3d/mkarpenko/WorldBox/Player.log contains:
# [IdleSpectator]: Harmony patches applied
# Fail fast if log shows: error CS
```
Do **not** call `steam -applaunch` (or `./scripts/harness-run.sh` when the game is down) from a sandboxed Shell - it fails silently from the user's POV.
## Definition of done
- Relevant harness scenario(s) PASS

View file

@ -45,6 +45,8 @@ Use harness `fast_timing` + `director_run` / `age_current` for scheduling tests.
| Flaky health/no_bad mid-scenario | ambient retarget while harness busy | `FreezeDirector` unless `director_run` |
| Steam hang after tests | `Application.Quit` | leave game running |
| Director tests take forever | production dwell/rotate | `fast_timing` (not game 5x) |
| Game never opens / `Cannot run as root user` | Shell sandboxed; Steam rejects root-like env | re-run launch/`harness-run.sh` with Shell `required_permissions: ["all"]` |
| Old mod still loaded after edit | NML caches until restart | kill `.../common/worldbox/worldbox`, bump `mod.json`, relaunch with `all` perms |
## Useful asserts

View file

@ -1026,6 +1026,15 @@ public static class AgentHarness
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);

View file

@ -0,0 +1,17 @@
using HarmonyLib;
namespace IdleSpectator;
/// <summary>
/// After vanilla hover clears <see cref="UnitSelectionEffect.last_actor"/>,
/// re-pin the Idle Spectator focus so destination / lover / attack arrows still draw.
/// </summary>
[HarmonyPatch(typeof(UnitSelectionEffect), nameof(UnitSelectionEffect.update))]
internal static class FocusArrowPatches
{
[HarmonyPostfix]
private static void PinFocusAfterVanillaHover(UnitSelectionEffect __instance, float pElapsed)
{
FocusRelationshipArrows.PinFocusActor();
}
}

View file

@ -0,0 +1,55 @@
namespace IdleSpectator;
/// <summary>
/// While Idle Spectator focuses a unit, reuse vanilla destination / lover / attack
/// cursor arrows by pinning <see cref="UnitSelectionEffect.last_actor"/>.
/// Arrow visibility follows the player's existing PlayerConfig toggles
/// (<c>cursor_arrow_destination</c>, <c>cursor_arrow_lover</c>,
/// <c>cursor_arrow_attack_target</c>) - idle does not force them on or off.
/// </summary>
internal static class FocusRelationshipArrows
{
public static void OnSpectatorEnabled()
{
PinFocusActor();
}
public static void OnSpectatorDisabled()
{
UnitSelectionEffect.setLastActor(null);
}
/// <summary>
/// Keep the focused unit as the vanilla "hovered" actor so relationship arrows draw
/// when the matching cursor-arrow options are already enabled.
/// Safe to call every frame; no-op when idle is off or there is no focus.
/// </summary>
public static void PinFocusActor()
{
if (!SpectatorMode.Active || !MoveCamera.hasFocusUnit())
{
return;
}
Actor focus = MoveCamera._focus_unit;
if (focus == null || !focus.isAlive())
{
return;
}
UnitSelectionEffect.setLastActor(focus);
}
public static bool IsPinnedToFocus()
{
if (!SpectatorMode.Active || !MoveCamera.hasFocusUnit())
{
return false;
}
Actor focus = MoveCamera._focus_unit;
return focus != null
&& focus.isAlive()
&& UnitSelectionEffect.last_actor == focus;
}
}

View file

@ -131,6 +131,7 @@ internal static class HarnessScenarios
Step("c26", "assert", expect: "health"),
Step("c27", "assert", expect: "power_bar", value: "false"),
Step("c28", "assert", expect: "no_bad"),
Step("c29", "assert", expect: "focus_arrows"),
Step("c30", "watch", asset: "auto", label: "New species: {asset}", tier: "Curiosity"),
Step("c31", "wait", wait: 0.3f),

View file

@ -74,6 +74,7 @@ public class ModClass : MonoBehaviour, IMod, IConfigurable, ILocalizable
WatchCaption.Update();
Chronicle.Update();
FocusRelationshipArrows.PinFocusActor();
StateProbe.Update();
AutoTest.Update();
}

View file

@ -39,6 +39,7 @@ public static class SpectatorMode
InterestDirector.OnSpectatorEnabled();
SpeciesDiscovery.OnSpectatorEnabled();
Chronicle.OnSpectatorEnabled();
FocusRelationshipArrows.OnSpectatorEnabled();
WorldTip.showNowTop("Idle Spectator ON (I or any input to stop; F9 chronicle)", pTranslate: false);
LogService.LogInfo("[IdleSpectator] Spectator mode enabled");
}
@ -46,6 +47,7 @@ public static class SpectatorMode
{
InterestDirector.OnSpectatorDisabled();
SpeciesDiscovery.OnSpectatorDisabled();
FocusRelationshipArrows.OnSpectatorDisabled();
CameraDirector.ClearFollow();
WatchCaption.Clear();
StateProbe.Reset();

View file

@ -1,7 +1,7 @@
{
"name": "IdleSpectator",
"author": "dazed",
"version": "0.9.20",
"description": "AFK Idle Spectator (I) + Chronicle (F9). Full trait names; rarity-ranked; E2E trait assert.",
"version": "0.9.22",
"description": "AFK Idle Spectator (I) + Chronicle (F9). Focus arrows respect vanilla toggles.",
"GUID": "com.dazed.idlespectator"
}