arrows for idle mode
This commit is contained in:
parent
9ae7f7ffa1
commit
9fdce47810
9 changed files with 122 additions and 2 deletions
|
|
@ -152,6 +152,39 @@ Use `fast_timing` / `director_run` instead.
|
||||||
- Prefer `./scripts/harness-run.sh`; avoid `Application.Quit`.
|
- Prefer `./scripts/harness-run.sh`; avoid `Application.Quit`.
|
||||||
- After code changes, restart/relaunch so NML recompiles.
|
- 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
|
## Definition of done
|
||||||
|
|
||||||
- Relevant harness scenario(s) PASS
|
- Relevant harness scenario(s) PASS
|
||||||
|
|
|
||||||
|
|
@ -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` |
|
| Flaky health/no_bad mid-scenario | ambient retarget while harness busy | `FreezeDirector` unless `director_run` |
|
||||||
| Steam hang after tests | `Application.Quit` | leave game running |
|
| Steam hang after tests | `Application.Quit` | leave game running |
|
||||||
| Director tests take forever | production dwell/rotate | `fast_timing` (not game 5x) |
|
| 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
|
## Useful asserts
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1026,6 +1026,15 @@ public static class AgentHarness
|
||||||
detail = $"remembered={_rememberedFocusKey} now={nowKey}";
|
detail = $"remembered={_rememberedFocusKey} now={nowKey}";
|
||||||
break;
|
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":
|
case "presented":
|
||||||
{
|
{
|
||||||
string assetId = ResolveAssetId(cmd);
|
string assetId = ResolveAssetId(cmd);
|
||||||
|
|
|
||||||
17
IdleSpectator/FocusArrowPatches.cs
Normal file
17
IdleSpectator/FocusArrowPatches.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
55
IdleSpectator/FocusRelationshipArrows.cs
Normal file
55
IdleSpectator/FocusRelationshipArrows.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -131,6 +131,7 @@ internal static class HarnessScenarios
|
||||||
Step("c26", "assert", expect: "health"),
|
Step("c26", "assert", expect: "health"),
|
||||||
Step("c27", "assert", expect: "power_bar", value: "false"),
|
Step("c27", "assert", expect: "power_bar", value: "false"),
|
||||||
Step("c28", "assert", expect: "no_bad"),
|
Step("c28", "assert", expect: "no_bad"),
|
||||||
|
Step("c29", "assert", expect: "focus_arrows"),
|
||||||
|
|
||||||
Step("c30", "watch", asset: "auto", label: "New species: {asset}", tier: "Curiosity"),
|
Step("c30", "watch", asset: "auto", label: "New species: {asset}", tier: "Curiosity"),
|
||||||
Step("c31", "wait", wait: 0.3f),
|
Step("c31", "wait", wait: 0.3f),
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@ public class ModClass : MonoBehaviour, IMod, IConfigurable, ILocalizable
|
||||||
|
|
||||||
WatchCaption.Update();
|
WatchCaption.Update();
|
||||||
Chronicle.Update();
|
Chronicle.Update();
|
||||||
|
FocusRelationshipArrows.PinFocusActor();
|
||||||
StateProbe.Update();
|
StateProbe.Update();
|
||||||
AutoTest.Update();
|
AutoTest.Update();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ public static class SpectatorMode
|
||||||
InterestDirector.OnSpectatorEnabled();
|
InterestDirector.OnSpectatorEnabled();
|
||||||
SpeciesDiscovery.OnSpectatorEnabled();
|
SpeciesDiscovery.OnSpectatorEnabled();
|
||||||
Chronicle.OnSpectatorEnabled();
|
Chronicle.OnSpectatorEnabled();
|
||||||
|
FocusRelationshipArrows.OnSpectatorEnabled();
|
||||||
WorldTip.showNowTop("Idle Spectator ON (I or any input to stop; F9 chronicle)", pTranslate: false);
|
WorldTip.showNowTop("Idle Spectator ON (I or any input to stop; F9 chronicle)", pTranslate: false);
|
||||||
LogService.LogInfo("[IdleSpectator] Spectator mode enabled");
|
LogService.LogInfo("[IdleSpectator] Spectator mode enabled");
|
||||||
}
|
}
|
||||||
|
|
@ -46,6 +47,7 @@ public static class SpectatorMode
|
||||||
{
|
{
|
||||||
InterestDirector.OnSpectatorDisabled();
|
InterestDirector.OnSpectatorDisabled();
|
||||||
SpeciesDiscovery.OnSpectatorDisabled();
|
SpeciesDiscovery.OnSpectatorDisabled();
|
||||||
|
FocusRelationshipArrows.OnSpectatorDisabled();
|
||||||
CameraDirector.ClearFollow();
|
CameraDirector.ClearFollow();
|
||||||
WatchCaption.Clear();
|
WatchCaption.Clear();
|
||||||
StateProbe.Reset();
|
StateProbe.Reset();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "IdleSpectator",
|
"name": "IdleSpectator",
|
||||||
"author": "dazed",
|
"author": "dazed",
|
||||||
"version": "0.9.20",
|
"version": "0.9.22",
|
||||||
"description": "AFK Idle Spectator (I) + Chronicle (F9). Full trait names; rarity-ranked; E2E trait assert.",
|
"description": "AFK Idle Spectator (I) + Chronicle (F9). Focus arrows respect vanilla toggles.",
|
||||||
"GUID": "com.dazed.idlespectator"
|
"GUID": "com.dazed.idlespectator"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue