Update IdleSpectator to version 0.16.8. Introduce new functionality for managing long nametags in WatchCaption, ensuring they do not overlap with level badges. Add new harness scenarios for testing nametag layout and headline display. Revise existing harness commands for improved clarity and control. Increment version in mod.json to reflect these changes.
This commit is contained in:
parent
1d2e830013
commit
8427079188
6 changed files with 314 additions and 13 deletions
|
|
@ -17,10 +17,19 @@ Do not call `Application.Quit()` (can hang Steam on Linux).
|
|||
|
||||
**Never start, relaunch, or kill WorldBox / Steam from the Cursor sandbox.**
|
||||
|
||||
- `scripts/harness-run.sh` may launch the game when it is not already running.
|
||||
- `pkill`, Steam, and game process control also fail or misbehave under sandbox.
|
||||
- Always run harness commands (and any WorldBox process control) with **unrestricted / `all` permissions** so they execute outside the sandbox.
|
||||
- Prefer `--no-launch` only when WorldBox is already running *and* you still use unrestricted permissions for the runner itself (log tail, `.harness` I/O under the live mod path).
|
||||
- Always run harness commands and WorldBox process control with **unrestricted / `all` permissions**.
|
||||
- Canonical process control (do not invent ad-hoc kill/launch loops):
|
||||
|
||||
```bash
|
||||
./scripts/worldbox-ctl.sh status
|
||||
./scripts/worldbox-ctl.sh start
|
||||
./scripts/worldbox-ctl.sh stop
|
||||
./scripts/worldbox-ctl.sh restart # after mod.json bump / NML recompile
|
||||
```
|
||||
|
||||
- `scripts/harness-run.sh` may launch the game when it is not already running (same Steam app id).
|
||||
- Prefer `--no-launch` only when `./scripts/worldbox-ctl.sh status` shows WorldBox is already up.
|
||||
- **Never** `pkill -f` with a `worldbox` path in the agent shell command - it can match and kill the agent process itself. Use `worldbox-ctl.sh stop` (`killall worldbox`).
|
||||
|
||||
## WorldBox wiki (mechanics source of truth)
|
||||
|
||||
|
|
@ -38,18 +47,22 @@ Vanilla `Tooltip` / `TooltipLibrary` (`tooltips/tooltip_actor`, etc.) may be reu
|
|||
After any feature or bug fix in `IdleSpectator/`:
|
||||
|
||||
1. Bump `IdleSpectator/mod.json` version (NML recompiles on game load).
|
||||
2. Extend or add a scenario in `IdleSpectator/HarnessScenarios.cs` when the change is new behavior.
|
||||
3. Day-to-day / PR gate:
|
||||
2. If the game was already running, recompile with:
|
||||
```bash
|
||||
./scripts/worldbox-ctl.sh restart
|
||||
```
|
||||
3. Extend or add a scenario in `IdleSpectator/HarnessScenarios.cs` when the change is new behavior.
|
||||
4. Day-to-day / PR gate:
|
||||
```bash
|
||||
./scripts/harness-run.sh --repeat 3 critical_smoke
|
||||
```
|
||||
4. After larger changes (director, discovery, settings, input, tips, chronicle), also run:
|
||||
5. After larger changes (director, discovery, settings, input, tips, chronicle), also run:
|
||||
```bash
|
||||
./scripts/harness-run.sh regression
|
||||
```
|
||||
5. If you added a focused scenario, run it directly too.
|
||||
6. Iterate until **all repeats PASS**. Do not stop after one flaky pass.
|
||||
7. Report: scenario name(s), PASS/FAIL counts, and any new asserts.
|
||||
6. If you added a focused scenario, run it directly too.
|
||||
7. Iterate until **all repeats PASS**. Do not stop after one flaky pass.
|
||||
8. Report: scenario name(s), PASS/FAIL counts, and any new asserts.
|
||||
|
||||
### HUD / visual changes - screenshot gate (required)
|
||||
|
||||
|
|
|
|||
|
|
@ -500,6 +500,17 @@ public static class AgentHarness
|
|||
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();
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ internal static class HarnessScenarios
|
|||
case "critical":
|
||||
case "critical_smoke":
|
||||
return CriticalSmoke();
|
||||
case "dossier_nametag":
|
||||
case "nametag_layout":
|
||||
return DossierNametagLayout();
|
||||
case "settings_full":
|
||||
return SettingsFull();
|
||||
case "ghost_guard":
|
||||
|
|
@ -442,6 +445,13 @@ internal static class HarnessScenarios
|
|||
Step("c47", "assert", expect: "no_bad"),
|
||||
Step("c48", "assert", expect: "power_bar", value: "false"),
|
||||
|
||||
// Long nametag must not paint under the level badge.
|
||||
Step("c49a", "dossier_force_headline", value: "Zezqzo Tikzkek Tak (scorpion)"),
|
||||
Step("c49b", "assert", expect: "caption_layout_ok"),
|
||||
Step("c49c", "assert", expect: "dossier_contains", value: "Zezqzo"),
|
||||
Step("c49d", "screenshot", value: "hud-dossier-long-name.png"),
|
||||
Step("c49e", "wait", wait: 0.35f),
|
||||
|
||||
Step("c50", "simulate_input"),
|
||||
Step("c51", "wait", wait: 0.25f),
|
||||
Step("c52", "assert", expect: "idle", value: "false"),
|
||||
|
|
@ -452,6 +462,37 @@ internal static class HarnessScenarios
|
|||
};
|
||||
}
|
||||
|
||||
/// <summary>Long "Name (species)" must leave room for the level chip (no overlap).</summary>
|
||||
private static List<HarnessCommand> DossierNametagLayout()
|
||||
{
|
||||
return new List<HarnessCommand>
|
||||
{
|
||||
Step("dn0", "dismiss_windows"),
|
||||
Step("dn1", "wait_world"),
|
||||
Step("dn2", "set_setting", expect: "enabled", value: "true"),
|
||||
Step("dn3", "set_setting", expect: "show_dossier_caption", value: "true"),
|
||||
Step("dn4", "pick_unit", asset: "auto"),
|
||||
Step("dn5", "spectator", value: "off"),
|
||||
Step("dn6", "spectator", value: "on"),
|
||||
Step("dn7", "focus", asset: "auto"),
|
||||
Step("dn8", "wait", wait: 0.3f),
|
||||
Step("dn10", "dossier_force_headline", value: "Zezqzo Tikzkek Tak (scorpion)"),
|
||||
Step("dn11", "assert", expect: "dossier_contains", value: "Zezqzo"),
|
||||
Step("dn12", "assert", expect: "caption_layout_ok"),
|
||||
Step("dn13", "screenshot", value: "hud-dossier-long-name.png"),
|
||||
Step("dn14", "wait", wait: 0.4f),
|
||||
// Extremely long: must ellipsis rather than paint under level.
|
||||
Step("dn20", "dossier_force_headline",
|
||||
value: "Zezqzo Tikzkek Tak the Unbelievably Long-Named Elder (scorpion)"),
|
||||
Step("dn21", "assert", expect: "caption_layout_ok"),
|
||||
Step("dn22", "assert", expect: "dossier_contains", value: "Zezqzo"),
|
||||
Step("dn23", "screenshot", value: "hud-dossier-long-name-ellipsis.png"),
|
||||
Step("dn24", "wait", wait: 0.4f),
|
||||
Step("dn90", "assert", expect: "no_bad"),
|
||||
Step("dn99", "snapshot"),
|
||||
};
|
||||
}
|
||||
|
||||
private static List<HarnessCommand> SettingsFull()
|
||||
{
|
||||
return new List<HarnessCommand>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ public static class WatchCaption
|
|||
private const float TraitIcon = 12f;
|
||||
private const float HistoryIcon = 10f;
|
||||
private const float HistoryColMinW = 118f;
|
||||
private const float NameMinW = 28f;
|
||||
/// <summary>Hard cap so long "Name (species)" lines cannot paint under the level chip.</summary>
|
||||
private const float NameMaxW = 210f;
|
||||
private const float PadX = 4f;
|
||||
/// <summary>Inset from the canvas top-right corner (grows left via pivot).</summary>
|
||||
private const float ScreenInset = 12f;
|
||||
|
|
@ -759,10 +762,36 @@ public static class WatchCaption
|
|||
return _current != null ? (_current.TaskText ?? "") : "";
|
||||
}
|
||||
|
||||
/// <summary>Harness: text currently shown on the nametag (may be ellipsis-truncated).</summary>
|
||||
public static string ShownNameChipText =>
|
||||
_nameText != null && _nameText.gameObject.activeSelf ? (_nameText.text ?? "") : "";
|
||||
|
||||
/// <summary>Harness: text currently shown on the nametag task chip.</summary>
|
||||
public static string ShownTaskChipText =>
|
||||
_taskText != null && _taskText.gameObject.activeSelf ? (_taskText.text ?? "") : "";
|
||||
|
||||
/// <summary>Harness: force a long nametag headline and relayout (layout overlap tests).</summary>
|
||||
public static void ForceNametagHeadline(string headline)
|
||||
{
|
||||
EnsureBuilt();
|
||||
string text = string.IsNullOrEmpty(headline) ? "Nobody" : headline.Trim();
|
||||
LastHeadline = text;
|
||||
if (_nameText != null)
|
||||
{
|
||||
_nameText.text = text;
|
||||
}
|
||||
|
||||
if (_current != null)
|
||||
{
|
||||
_current.Headline = text;
|
||||
}
|
||||
|
||||
bool hasBody = _current != null && _current.UnitId != 0;
|
||||
bool hasTask = _taskText != null && _taskText.gameObject.activeSelf;
|
||||
bool hasReason = _reasonText != null && _reasonText.gameObject.activeSelf;
|
||||
Relayout(hasBody, CountActiveTraitSlots(), hasTask, hasReason, CountActiveHistorySlots());
|
||||
}
|
||||
|
||||
private static void BringHeaderFront()
|
||||
{
|
||||
// Draw nametag above vanilla avatar chrome (which can paint outside its host).
|
||||
|
|
@ -1520,7 +1549,7 @@ public static class WatchCaption
|
|||
|
||||
if (_nameText != null)
|
||||
{
|
||||
float nameW = Mathf.Clamp(MeasureTextWidth(_nameText, 72f), 28f, 120f);
|
||||
float nameW = FitNameChipWidth();
|
||||
PlaceLeftChip(_nameText.rectTransform, x, yFromTop, nameW, HeaderH);
|
||||
x += nameW + 5f;
|
||||
}
|
||||
|
|
@ -1583,7 +1612,7 @@ public static class WatchCaption
|
|||
float x = PadX + SpeciesSize + 3f;
|
||||
if (_nameText != null)
|
||||
{
|
||||
x += Mathf.Clamp(MeasureTextWidth(_nameText, 72f), 28f, 120f) + 5f;
|
||||
x += FitNameChipWidth() + 5f;
|
||||
}
|
||||
|
||||
if (_levelIcon != null && _levelIcon.gameObject.activeSelf)
|
||||
|
|
@ -1680,6 +1709,60 @@ public static class WatchCaption
|
|||
rt.sizeDelta = new Vector2(width, height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Size the name chip to the visible text and ellipsis-truncate when over <see cref="NameMaxW"/>.
|
||||
/// Prevents Overflow paint from landing under the level badge (long "Name (species)" lines).
|
||||
/// </summary>
|
||||
private static float FitNameChipWidth()
|
||||
{
|
||||
if (_nameText == null)
|
||||
{
|
||||
return NameMinW;
|
||||
}
|
||||
|
||||
_nameText.resizeTextForBestFit = false;
|
||||
_nameText.horizontalOverflow = HorizontalWrapMode.Overflow;
|
||||
string full = !string.IsNullOrEmpty(LastHeadline) ? LastHeadline : (_nameText.text ?? "");
|
||||
if (string.IsNullOrEmpty(full))
|
||||
{
|
||||
_nameText.text = "";
|
||||
return NameMinW;
|
||||
}
|
||||
|
||||
_nameText.text = full;
|
||||
float natural = MeasureTextWidth(_nameText, 72f);
|
||||
if (natural <= NameMaxW)
|
||||
{
|
||||
return Mathf.Clamp(natural, NameMinW, NameMaxW);
|
||||
}
|
||||
|
||||
const string ellipsis = "...";
|
||||
int lo = 0;
|
||||
int hi = full.Length;
|
||||
string best = ellipsis;
|
||||
while (lo <= hi)
|
||||
{
|
||||
int mid = (lo + hi) / 2;
|
||||
string candidate = mid <= 0
|
||||
? ellipsis
|
||||
: (mid >= full.Length ? full : full.Substring(0, mid).TrimEnd() + ellipsis);
|
||||
_nameText.text = candidate;
|
||||
float w = MeasureTextWidth(_nameText, NameMaxW);
|
||||
if (w <= NameMaxW)
|
||||
{
|
||||
best = candidate;
|
||||
lo = mid + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
hi = mid - 1;
|
||||
}
|
||||
}
|
||||
|
||||
_nameText.text = best;
|
||||
return Mathf.Clamp(MeasureTextWidth(_nameText, NameMaxW), NameMinW, NameMaxW);
|
||||
}
|
||||
|
||||
private static float MeasureTextWidth(Text text, float fallback)
|
||||
{
|
||||
if (text == null || string.IsNullOrEmpty(text.text))
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "IdleSpectator",
|
||||
"author": "dazed",
|
||||
"version": "0.16.6",
|
||||
"version": "0.16.8",
|
||||
"description": "AFK Idle Spectator (I) + Lore (L). Event-centered scene director + happiness Activity/Life logging.",
|
||||
"GUID": "com.dazed.idlespectator"
|
||||
}
|
||||
|
|
|
|||
153
scripts/worldbox-ctl.sh
Executable file
153
scripts/worldbox-ctl.sh
Executable file
|
|
@ -0,0 +1,153 @@
|
|||
#!/usr/bin/env bash
|
||||
# Canonical WorldBox process control for IdleSpectator agents.
|
||||
# Always run this script (or harness-run.sh) with unrestricted / `all` permissions.
|
||||
# Never invent ad-hoc pkill -f patterns that include the worldbox path in the
|
||||
# agent shell command line - that can kill the agent itself.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/worldbox-ctl.sh status
|
||||
# ./scripts/worldbox-ctl.sh start
|
||||
# ./scripts/worldbox-ctl.sh stop
|
||||
# ./scripts/worldbox-ctl.sh restart # stop + start + wait for IdleSpectator ready
|
||||
set -euo pipefail
|
||||
|
||||
STEAM_APP_ID="${WORLDBOX_APP_ID:-1206560}"
|
||||
LOG="${WORLDBOX_LOG:-$HOME/.config/unity3d/mkarpenko/WorldBox/Player.log}"
|
||||
BOOT_TIMEOUT_SEC="${HARNESS_BOOT_TIMEOUT:-180}"
|
||||
LAUNCH_LOG="${WORLDBOX_LAUNCH_LOG:-/tmp/idle-spectator-harness-launch.log}"
|
||||
|
||||
worldbox_pids() {
|
||||
# Match the real game binary only (process name "worldbox").
|
||||
# Do NOT use pkill -f with a path string that also appears in the calling shell.
|
||||
ps -C worldbox -o pid= 2>/dev/null | tr -s ' ' | sed '/^$/d' || true
|
||||
}
|
||||
|
||||
worldbox_running() {
|
||||
local pids
|
||||
pids="$(worldbox_pids)"
|
||||
[[ -n "${pids// /}" ]]
|
||||
}
|
||||
|
||||
mod_ready() {
|
||||
[[ -f "$LOG" ]] || return 1
|
||||
if rg -q "error CS" "$LOG" 2>/dev/null \
|
||||
&& rg -q "Compile Mod IdleSpectator|IdleSpectator.*error CS|error CS.*IdleSpectator" "$LOG" 2>/dev/null; then
|
||||
return 2
|
||||
fi
|
||||
rg -q "\[IdleSpectator\]: Harmony patches applied" "$LOG" 2>/dev/null
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
if worldbox_running; then
|
||||
echo "WorldBox running pids=$(worldbox_pids | tr '\n' ' ')"
|
||||
if mod_ready; then
|
||||
echo "IdleSpectator: ready (Harmony patches applied)"
|
||||
else
|
||||
echo "IdleSpectator: not ready yet (check Player.log)"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
echo "WorldBox not running"
|
||||
return 1
|
||||
}
|
||||
|
||||
cmd_stop() {
|
||||
if ! worldbox_running; then
|
||||
echo "WorldBox already stopped"
|
||||
return 0
|
||||
fi
|
||||
echo "Stopping WorldBox ..."
|
||||
# killall by process name - safe for agent shells (no -f path match).
|
||||
killall -q worldbox 2>/dev/null || true
|
||||
local i
|
||||
for i in $(seq 1 30); do
|
||||
if ! worldbox_running; then
|
||||
echo "WorldBox stopped"
|
||||
return 0
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
echo "WorldBox still running after killall; sending SIGKILL ..." >&2
|
||||
killall -q -9 worldbox 2>/dev/null || true
|
||||
sleep 1
|
||||
if worldbox_running; then
|
||||
echo "FAILED to stop WorldBox pids=$(worldbox_pids | tr '\n' ' ')" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "WorldBox stopped"
|
||||
}
|
||||
|
||||
cmd_start() {
|
||||
if worldbox_running; then
|
||||
echo "WorldBox already running"
|
||||
return 0
|
||||
fi
|
||||
echo "Starting WorldBox (steam app $STEAM_APP_ID) ..."
|
||||
echo "===== WORLDBOX-CTL START $(date -Iseconds) =====" >>"$LAUNCH_LOG" 2>/dev/null || true
|
||||
nohup steam -applaunch "$STEAM_APP_ID" >>"$LAUNCH_LOG" 2>&1 &
|
||||
disown || true
|
||||
|
||||
local deadline=$((SECONDS + BOOT_TIMEOUT_SEC))
|
||||
local saw=0
|
||||
while (( SECONDS < deadline )); do
|
||||
if worldbox_running; then
|
||||
saw=1
|
||||
local ready_rc=0
|
||||
mod_ready || ready_rc=$?
|
||||
if (( ready_rc == 0 )); then
|
||||
echo "WorldBox + IdleSpectator ready"
|
||||
return 0
|
||||
fi
|
||||
if (( ready_rc == 2 )); then
|
||||
echo "IdleSpectator compile failed - see $LOG" >&2
|
||||
rg -n "error CS|Compile Mod IdleSpectator" "$LOG" 2>/dev/null | tail -n 40 >&2 || true
|
||||
return 1
|
||||
fi
|
||||
printf '.'
|
||||
else
|
||||
printf 'o'
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo >&2
|
||||
if (( saw == 0 )); then
|
||||
echo "Timed out waiting for WorldBox process (${BOOT_TIMEOUT_SEC}s)" >&2
|
||||
else
|
||||
echo "Timed out waiting for IdleSpectator ready (${BOOT_TIMEOUT_SEC}s)" >&2
|
||||
fi
|
||||
tail -n 40 "$LOG" 2>/dev/null >&2 || true
|
||||
return 1
|
||||
}
|
||||
|
||||
cmd_restart() {
|
||||
cmd_stop
|
||||
sleep 2
|
||||
cmd_start
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 {status|start|stop|restart}
|
||||
|
||||
Canonical WorldBox control for IdleSpectator.
|
||||
Always invoke with unrestricted / all permissions (never from the Cursor sandbox).
|
||||
EOF
|
||||
}
|
||||
|
||||
main() {
|
||||
local op="${1:-}"
|
||||
case "$op" in
|
||||
status) cmd_status ;;
|
||||
start) cmd_start ;;
|
||||
stop) cmd_stop ;;
|
||||
restart) cmd_restart ;;
|
||||
-h|--help|help|"") usage; exit 2 ;;
|
||||
*)
|
||||
echo "Unknown op: $op" >&2
|
||||
usage
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Loading…
Reference in a new issue