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 <cursoragent@cursor.com>
This commit is contained in:
parent
c575c57190
commit
e4c00b4bbb
4 changed files with 245 additions and 6 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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`).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
225
scripts/soak-audit-player-log.sh
Executable file
225
scripts/soak-audit-player-log.sh
Executable file
|
|
@ -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
|
||||
Loading…
Reference in a new issue