From 9fdce4781090050d242b55c8c9e6a03d3f5975c8 Mon Sep 17 00:00:00 2001 From: DazedAnon Date: Tue, 14 Jul 2026 18:35:06 -0500 Subject: [PATCH] arrows for idle mode --- .cursor/skills/idle-spectator-e2e/SKILL.md | 33 +++++++++++ .../skills/idle-spectator-e2e/reference.md | 2 + IdleSpectator/AgentHarness.cs | 9 +++ IdleSpectator/FocusArrowPatches.cs | 17 ++++++ IdleSpectator/FocusRelationshipArrows.cs | 55 +++++++++++++++++++ IdleSpectator/HarnessScenarios.cs | 1 + IdleSpectator/Main.cs | 1 + IdleSpectator/SpectatorMode.cs | 2 + IdleSpectator/mod.json | 4 +- 9 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 IdleSpectator/FocusArrowPatches.cs create mode 100644 IdleSpectator/FocusRelationshipArrows.cs diff --git a/.cursor/skills/idle-spectator-e2e/SKILL.md b/.cursor/skills/idle-spectator-e2e/SKILL.md index 1dba1b8..5167bb5 100644 --- a/.cursor/skills/idle-spectator-e2e/SKILL.md +++ b/.cursor/skills/idle-spectator-e2e/SKILL.md @@ -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 + +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 diff --git a/.cursor/skills/idle-spectator-e2e/reference.md b/.cursor/skills/idle-spectator-e2e/reference.md index a94673e..e3520ce 100644 --- a/.cursor/skills/idle-spectator-e2e/reference.md +++ b/.cursor/skills/idle-spectator-e2e/reference.md @@ -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 diff --git a/IdleSpectator/AgentHarness.cs b/IdleSpectator/AgentHarness.cs index d303977..f626f68 100644 --- a/IdleSpectator/AgentHarness.cs +++ b/IdleSpectator/AgentHarness.cs @@ -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); diff --git a/IdleSpectator/FocusArrowPatches.cs b/IdleSpectator/FocusArrowPatches.cs new file mode 100644 index 0000000..a805bf0 --- /dev/null +++ b/IdleSpectator/FocusArrowPatches.cs @@ -0,0 +1,17 @@ +using HarmonyLib; + +namespace IdleSpectator; + +/// +/// After vanilla hover clears , +/// re-pin the Idle Spectator focus so destination / lover / attack arrows still draw. +/// +[HarmonyPatch(typeof(UnitSelectionEffect), nameof(UnitSelectionEffect.update))] +internal static class FocusArrowPatches +{ + [HarmonyPostfix] + private static void PinFocusAfterVanillaHover(UnitSelectionEffect __instance, float pElapsed) + { + FocusRelationshipArrows.PinFocusActor(); + } +} diff --git a/IdleSpectator/FocusRelationshipArrows.cs b/IdleSpectator/FocusRelationshipArrows.cs new file mode 100644 index 0000000..f0d8308 --- /dev/null +++ b/IdleSpectator/FocusRelationshipArrows.cs @@ -0,0 +1,55 @@ +namespace IdleSpectator; + +/// +/// While Idle Spectator focuses a unit, reuse vanilla destination / lover / attack +/// cursor arrows by pinning . +/// Arrow visibility follows the player's existing PlayerConfig toggles +/// (cursor_arrow_destination, cursor_arrow_lover, +/// cursor_arrow_attack_target) - idle does not force them on or off. +/// +internal static class FocusRelationshipArrows +{ + public static void OnSpectatorEnabled() + { + PinFocusActor(); + } + + public static void OnSpectatorDisabled() + { + UnitSelectionEffect.setLastActor(null); + } + + /// + /// 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. + /// + 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; + } +} diff --git a/IdleSpectator/HarnessScenarios.cs b/IdleSpectator/HarnessScenarios.cs index c84d7e9..97836de 100644 --- a/IdleSpectator/HarnessScenarios.cs +++ b/IdleSpectator/HarnessScenarios.cs @@ -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), diff --git a/IdleSpectator/Main.cs b/IdleSpectator/Main.cs index 42d98e4..d9711f0 100644 --- a/IdleSpectator/Main.cs +++ b/IdleSpectator/Main.cs @@ -74,6 +74,7 @@ public class ModClass : MonoBehaviour, IMod, IConfigurable, ILocalizable WatchCaption.Update(); Chronicle.Update(); + FocusRelationshipArrows.PinFocusActor(); StateProbe.Update(); AutoTest.Update(); } diff --git a/IdleSpectator/SpectatorMode.cs b/IdleSpectator/SpectatorMode.cs index b98bcb1..9c5be0e 100644 --- a/IdleSpectator/SpectatorMode.cs +++ b/IdleSpectator/SpectatorMode.cs @@ -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(); diff --git a/IdleSpectator/mod.json b/IdleSpectator/mod.json index 7be1ce8..69d3b78 100644 --- a/IdleSpectator/mod.json +++ b/IdleSpectator/mod.json @@ -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" }