From e4c00b4bbb0c7554aa5e91884ffdc0662bf9a3c0 Mon Sep 17 00:00:00 2001 From: DazedAnon Date: Thu, 16 Jul 2026 21:34:02 -0500 Subject: [PATCH] Add Player.log soak auditor and sync polish docs. Mark combat focus polish done and point next work at dossier caption/nametag lag. Co-authored-by: Cursor --- docs/camera-presentability-plan.md | 2 +- docs/event-audit.md | 14 +- docs/event-e2e.md | 10 ++ scripts/soak-audit-player-log.sh | 225 +++++++++++++++++++++++++++++ 4 files changed, 245 insertions(+), 6 deletions(-) create mode 100755 scripts/soak-audit-player-log.sh diff --git a/docs/camera-presentability-plan.md b/docs/camera-presentability-plan.md index 5c3604a..f7d32f4 100644 --- a/docs/camera-presentability-plan.md +++ b/docs/camera-presentability-plan.md @@ -179,7 +179,7 @@ Do not use Label rewrite as a substitute for the gate or class demotion. - no placeholder tip - no `no_focus_while_idle` BAD spike in a quiet idle window - fall class not dominating watches -3. Optional: lightweight Player.log auditor script or harness probe counting `identity_filter` after Watching tips in a timed idle soak. +3. Player.log soak auditor: `./scripts/soak-audit-player-log.sh [seconds]` (also `--since-bytes N`). ## Implementation status diff --git a/docs/event-audit.md b/docs/event-audit.md index 5e8ca69..4848cb5 100644 --- a/docs/event-audit.md +++ b/docs/event-audit.md @@ -78,9 +78,13 @@ Open rows are unexplained Wire candidates only. ## Idle quality -Presentability gate, ambient fall demotion, focus hold, and tip placeholder bans are done -(see [camera-presentability-plan.md](camera-presentability-plan.md)). +Done (see [camera-presentability-plan.md](camera-presentability-plan.md) and `combat_focus`): -**Polish next:** combat tip/focus alignment - camera follows the highest-scored living -participant and rewrites the Watching tip so the subject matches focus; clears "is fighting" -once combat goes cold. +- Presentability gate, ambient fall demotion, focus hold, tip placeholder ban +- Status-overlap hold (onset requires live status; offsets stay FixedDwell) +- Combat tip/focus alignment (highest-scored participant; tip subject matches focus; + clears "is fighting" when combat goes cold) + +**Polish next:** dossier caption/nametag sync if tip+focus match but the nametag briefly shows someone else. + +Soak audit helper: `./scripts/soak-audit-player-log.sh [seconds]` (or pass `--since-bytes N`). diff --git a/docs/event-e2e.md b/docs/event-e2e.md index 5ff7bdc..d564603 100644 --- a/docs/event-e2e.md +++ b/docs/event-e2e.md @@ -40,6 +40,16 @@ See [`camera-presentability-plan.md`](camera-presentability-plan.md). | No `Watching: something interesting` | `no_placeholder_tip` | | Focus survives max_cap | `camera_presentability` / `has_focus` | | Ambient building falls are B-only | live patch + pipelines `id_drop` | +| Status-overlap onset without live status | `status_overlap_camera` / `status_overlap_absent` | +| Combat tip subject matches focus | `combat_focus` / `tip_matches_focus` | +| Cold combat clears fight tip | `combat_focus` / `tip_not_contains` fighting | + +Live idle soak helper (Player.log window): + +```bash +./scripts/soak-audit-player-log.sh 60 +./scripts/soak-audit-player-log.sh --since-bytes "$(wc -c < ~/.config/unity3d/mkarpenko/WorldBox/Player.log)" +``` ## Coverage levels diff --git a/scripts/soak-audit-player-log.sh b/scripts/soak-audit-player-log.sh new file mode 100755 index 0000000..6f2ab69 --- /dev/null +++ b/scripts/soak-audit-player-log.sh @@ -0,0 +1,225 @@ +#!/usr/bin/env bash +# Audit IdleSpectator Watching / DROP / BAD / STATE from Player.log. +# +# Usage: +# ./scripts/soak-audit-player-log.sh # last 120s wall wait, then audit new bytes +# ./scripts/soak-audit-player-log.sh 60 # wait 60s then audit +# ./scripts/soak-audit-player-log.sh --since-bytes N # audit from byte offset (no wait) +# ./scripts/soak-audit-player-log.sh --no-wait # audit from current EOF backward? no - from mark file +# +# Env: +# PLAYER_LOG - override Player.log path +set -euo pipefail + +LOG="${PLAYER_LOG:-$HOME/.config/unity3d/mkarpenko/WorldBox/Player.log}" +SECONDS_WAIT=0 +SINCE_BYTES="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --since-bytes) + SINCE_BYTES="${2:-}" + shift 2 + ;; + --no-wait) + SECONDS_WAIT=0 + shift + ;; + -h|--help) + sed -n '2,12p' "$0" + exit 0 + ;; + *) + if [[ "$1" =~ ^[0-9]+$ ]]; then + SECONDS_WAIT="$1" + else + echo "Unknown arg: $1" >&2 + exit 2 + fi + shift + ;; + esac +done + +if [[ ! -f "$LOG" ]]; then + echo "Player.log not found: $LOG" >&2 + exit 1 +fi + +if [[ -z "$SINCE_BYTES" ]]; then + SINCE_BYTES=$(wc -c < "$LOG" | tr -d ' ') + if [[ "$SECONDS_WAIT" -gt 0 ]]; then + echo "Marked offset $SINCE_BYTES; waiting ${SECONDS_WAIT}s (leave Idle Spectator running) ..." + sleep "$SECONDS_WAIT" + else + echo "No wait and no --since-bytes: auditing from current EOF (empty window)." + echo "Tip: ./scripts/soak-audit-player-log.sh 60" + fi +fi + +python3 - "$LOG" "$SINCE_BYTES" <<'PY' +import re, sys +from collections import Counter +from pathlib import Path + +log_path = Path(sys.argv[1]) +armed = int(sys.argv[2]) +raw = log_path.read_bytes()[armed:].decode("utf-8", errors="replace") +lines = raw.splitlines() + +def harness_noise(ln: str) -> bool: + if "[HARNESS]" not in ln and "[ASSERT]" not in ln: + return False + return "action=" in ln or "batch done" in ln or "inbox reset" in ln + +watching = [] +pairs = [] +drops = [] +bads = [] +scenes = [] +states = [] +last = None + +for ln in lines: + if harness_noise(ln): + continue + if "IdleSpectator" not in ln: + continue + if "Watching:" in ln: + tip = ln.split("Watching:", 1)[1].strip() + tip = re.sub(r"^\[\d+\]\s*", "", tip) + watching.append(tip) + last = {"tip": tip, "unit": None, "caption": None} + pairs.append(last) + elif "[CAPTION]" in ln and last is not None: + last["caption"] = ln.split("[CAPTION]", 1)[1].strip() + elif "[DROP]" in ln: + drops.append(ln.split("[DROP]", 1)[1].strip()) + elif "[BAD]" in ln: + bads.append(ln.split("[BAD]", 1)[1].strip()) + elif "Scene end" in ln: + scenes.append(ln.split("Scene end", 1)[1].strip()) + elif "[STATE]" in ln: + states.append(ln) + if last is not None and last.get("unit") is None and "unit=" in ln: + last["unit"] = ln.split("unit=", 1)[1].strip() + +drop_kinds = Counter() +for d in drops: + drop_kinds[d.split(":", 1)[0].strip()] += 1 + +focus_true = focus_false = 0 +for s in states: + m = re.search(r"focus=(True|False)", s) + if not m: + continue + if m.group(1) == "False": + focus_false += 1 + else: + focus_true += 1 + +def bucket(t: str) -> str: + tl = t.lower() + if any(x in tl for x in ("fight", "battle", "in a fight")): + return "combat" + if any(x in tl for x in ("war", "kingdom", "rebellion")): + return "politics" + if any(x in tl for x in ("love", "smitten", "child", "parenthood", "mourns", "lover")): + return "relationship" + if any(x in tl for x in ("curs", "possess", "dream", "nightmare", "enchant", "stun")): + return "status" + if "new species" in tl or "appear" in tl or "hatch" in tl: + return "discovery" + if "plot" in tl or "decide" in tl: + return "plot" + if "fall" in tl and any(x in tl for x in ("tree", "plant", "bush", "flower", "maple")): + return "object_fall" + return "other" + +buckets = Counter(bucket(t) for t in watching) +scene_reasons = Counter() +for s in scenes: + m = re.match(r"\(([^)]+)\)", s) + if m: + scene_reasons[m.group(1)] += 1 + +identity = sum(1 for d in drops if "identity_filter" in d) +unpresentable = sum(1 for d in drops if "unpresentable" in d) +placeholder = sum(1 for t in watching if "something interesting" in t.lower()) +veg = [t for t in watching if bucket(t) == "object_fall"] +no_focus_bads = sum(1 for b in bads if "no focus" in b.lower()) + +mismatches = [] +sleep_lag = [] +for p in pairs: + tip = p["tip"] + unit = (p.get("unit") or "").strip() + cap = p.get("caption") or "" + if "fighting" in tip.lower() and "Sleeping" in cap: + sleep_lag.append((tip, cap)) + if not tip or not unit: + continue + if "fighting" not in tip.lower() and "fight of" not in tip.lower(): + continue + subj = tip.split(" is ", 1)[0].split(" ยท ", 1)[0].strip() + u0 = unit.split()[0] + if u0 and u0.lower() not in subj.lower(): + mismatches.append((tip, unit, cap[:72])) + +print(f"Player.log soak audit") +print(f" log={log_path}") +print(f" since_bytes={armed} chunk_lines={len(lines)}") +print() +print(f"Watching cuts: {len(watching)}") +print(f"Focus BADs: {no_focus_bads} (all BADs={len(bads)})") +print(f"focus True/False: {focus_true}/{focus_false}") +print(f"identity_filter: {identity}") +print(f"unpresentable: {unpresentable}") +print(f"placeholder tips: {placeholder}") +print(f"object falls: {len(veg)}") +print(f"fight tip/focus mismatches: {len(mismatches)}") +print(f"fighting+Sleeping lag: {len(sleep_lag)}") +print() +if buckets: + print("Buckets:") + for k, v in buckets.most_common(): + print(f" {k:16} {v}") + print() +if drop_kinds: + print("Drop kinds:") + for k, v in drop_kinds.most_common(12): + print(f" {k:24} {v}") + print() +if scene_reasons: + print("Scene ends:") + for k, v in scene_reasons.most_common(): + print(f" {k:16} {v}") + print() +if watching: + print("Tips:") + for t in watching: + print(f" - {t}") + print() +if mismatches: + print("Fight tip/focus mismatches:") + for tip, unit, cap in mismatches: + print(f" tip={tip!r} focus={unit!r} caption={cap!r}") + print() +if sleep_lag: + print("Fight tip + Sleeping caption:") + for tip, cap in sleep_lag: + print(f" tip={tip!r} caption={cap!r}") + print() + +ok = ( + no_focus_bads == 0 + and focus_false == 0 + and identity == 0 + and placeholder == 0 + and len(veg) == 0 + and len(mismatches) == 0 + and len(sleep_lag) == 0 +) +print("VERDICT:", "PASS" if ok else "REVIEW") +sys.exit(0 if ok else 1) +PY