Files
benilla-octo/scripts/vplsum.py
T
SamandClaude Opus 5.5 73427e2068 comments: the generators, the tools and the reference table headers
Docstrings keep their usage and exit codes; table headers keep the
column contract and the client addresses and lose the dates, the
correction history and every path into another repository. A
generated header changes in its generator's template, so each of the
four generators still reproduces its output byte for byte, and no data
row changed in any of the seven tables.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 19:32:13 +07:00

207 lines
8.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""Read a `vpl`/`bub` overlay trace and say where the motion came from: projection, solve or snap.
# walk a town with friendly plates on, tracing one line per plate per frame
WOW_WIN=1440x810 WOW_NOSOUND=1 WOW_GM=off \
WOW_UNATTENDED=1 WOW_USER=probeN WOW_PASS=pprobeN WOW_CHAR=Probe<n> \
WOW_PROBE_CHAT=".go xyz -9465 74 58 0" WOW_PROBE_CHAT_AT=10 \
WOW_PROBE_KEY="Shift@20:3;V@20.5:0.2;W@24:8" \
WOW_MOVE_TRACE=/tmp/vpl.trace WOW_MOVE_TRACE_TAGS=vpl \
WOW_PROBE_EXIT_AT=34 cargo run -q -p benilla
python3 scripts/vplsum.py /tmp/vpl.trace --from 24 --to 32 # the walk leg only
Jitter in a moving overlay has four causes the eye cannot tell apart: a noisy camera, a stale
anchor, the anti-overlap solve relocating a plate, or the pixel snap quantizing its glide. Every
`vpl` line carries the world anchor, the camera pose, the raw projected point, the solved rect and
the snapped rect, so telling them apart is arithmetic.
Read it in this order:
* projection smoothness: the raw projected point's frame-to-frame acceleration, per unit; small
(~0.05 px at 60 fps) clears the camera and the anchor.
* off-screen: plates drawn for a unit that is not on screen. Must be 0.
* solve jumps: a plate moving further than its own projection did; some is the reference's own
anti-overlap bounce, hundreds of pixels in a frame is a defect.
* snap: the pixel snap's extra per-frame displacement, at most half a device pixel; more means
the snap is on the wrong grid.
Pick one leg with `--from`/`--to`: a run is a login, a teleport, a stand and a walk, and their
average describes none of them.
It reads the chat bubble's `bub` lines too, detected from the file: same projector and
`overhead_anchor` as the plate but no anti-overlap solver, so there is no `solved=` stage and the
SOLVE section is skipped. The ANCHOR section matters most for it, because the bubble rides the
posed head attachment, which bobs with the run cycle:
WOW_MOVE_TRACE=/tmp/bub.trace WOW_MOVE_TRACE_TAGS=bub \
WOW_PROBE_CHAT="hello" WOW_PROBE_CHAT_AT=20 WOW_PROBE_KEY="W@20:6" ... cargo run -q -p benilla
python3 scripts/vplsum.py /tmp/bub.trace --from 20 --to 26
"""
import argparse
import re
import sys
from collections import defaultdict
# One regex for both overlays: `solved=` (the plate's anti-overlap solve) is optional, and the
# final rect is `plate=` or `frame=`.
LINE = re.compile(
r"t=\s*([\d.]+) (vpl|bub)\s+e=(\d+) vp=\((\d+),(\d+)\) anchor=\[([-\d.,e]+)\] "
r"cam=\[([-\d.,e]+)\] fwd=\[([-\d.,e]+)\] scr=\(([-\d.e]+),([-\d.e]+)\) "
r"(?:solved=\(([-\d.e]+),([-\d.e]+)\) )?(?:plate|frame)=\(([-\d.e]+),([-\d.e]+)\)"
)
# A step this far past the plate's own projected motion is a jump, not a glide: about a quarter of
# the plate's height, the smallest relocation a viewer reads as the plate moving by itself.
JUMP_PX = 4.0
# Frames further apart than this are a gap (the plate blinked out), never differenced.
GAP_S = 0.1
def vec3(s):
return [float(v) for v in s.split(",")]
def load(path, lo, hi):
"""-> (tag, viewport, {entity: [(t, anchor, cam, scr, solved, rect)]}, {t: overlay count})
A bubble line has no solver stage, so its `solved` slot mirrors the raw point.
"""
rows, frames, vp, tag = defaultdict(list), defaultdict(int), None, None
for line in open(path):
m = LINE.match(line)
if not m:
continue
t = float(m.group(1))
if not (lo <= t <= hi):
continue
tag = m.group(2)
vp = (float(m.group(4)), float(m.group(5)))
frames[t] += 1
scr = (float(m.group(9)), float(m.group(10)))
solved = (
(float(m.group(11)), float(m.group(12)))
if m.group(11) is not None
else scr
)
rows[int(m.group(3))].append(
(
t,
vec3(m.group(6)),
vec3(m.group(7)),
scr,
solved,
(float(m.group(13)), float(m.group(14))),
)
)
for s in rows.values():
s.sort()
return tag, vp, rows, frames
def stat(vals, fmt="{:6.3f}"):
if not vals:
return " —"
v = sorted(abs(x) for x in vals)
return " ".join(
fmt.format(x) for x in (v[len(v) // 2], v[int(0.9 * (len(v) - 1))], v[-1])
)
def on_screen(p, vp):
return 0.0 <= p[0] <= vp[0] and 0.0 <= p[1] <= vp[1]
def main():
ap = argparse.ArgumentParser()
ap.add_argument("trace")
ap.add_argument("--from", dest="lo", type=float, default=0.0)
ap.add_argument("--to", dest="hi", type=float, default=1e9)
ap.add_argument("--worst", type=int, default=10, help="how many jumps to name")
a = ap.parse_args()
tag, vp, rows, frames = load(a.trace, a.lo, a.hi)
if not rows:
sys.exit(
f"{a.trace}: no `vpl`/`bub` lines in [{a.lo}, {a.hi}] — "
"was WOW_MOVE_TRACE_TAGS=vpl (or =bub) set?"
)
what = "plates" if tag == "vpl" else "bubbles"
ts = sorted(frames)
dts = sorted(b - a_ for a_, b in zip(ts, ts[1:]) if b - a_ < GAP_S)
drawn = sum(len(s) for s in rows.values())
off = sum(1 for s in rows.values() for r in s if not on_screen(r[3], vp))
print(
f"viewport {vp[0]:.0f}x{vp[1]:.0f} frames {len(ts)} in [{ts[0]:.2f},{ts[-1]:.2f}] "
f"dt med {1000 * dts[len(dts) // 2]:.1f} ms {what}/frame {drawn / len(ts):.2f}"
)
print(f"OFF-SCREEN {what} drawn: {off}/{drawn} ({100 * off / drawn:.1f}%) [must be 0]")
snap, jumps, steps = [], [], 0
for e, s in rows.items():
for p, q in zip(s, s[1:]):
if q[0] - p[0] > GAP_S:
continue
steps += 1
d_scr = [q[3][i] - p[3][i] for i in (0, 1)]
d_sol = [q[4][i] - p[4][i] for i in (0, 1)]
d_pl = [q[5][i] - p[5][i] for i in (0, 1)]
snap.append(max(abs(d_pl[i] - d_sol[i]) for i in (0, 1)))
solve = max(abs(d_sol[i] - d_scr[i]) for i in (0, 1))
if solve >= JUMP_PX:
jumps.append((solve, q[0], e, p[5], q[5], p[3], q[3]))
print(f"SNAP extra px/frame (med p90 max): {stat(snap)} [≤ half a device pixel]")
if tag == "vpl":
print(
f"SOLVE jumps ≥ {JUMP_PX:.0f} px: {len(jumps)}/{steps} steps "
f"({100 * len(jumps) / max(steps, 1):.2f}%)"
)
for j in sorted(jumps, reverse=True)[: a.worst]:
print(
f" t={j[1]:8.3f} e={j[2]:<5d} {j[0]:7.1f} px "
f"plate ({j[3][0]:7.1f},{j[3][1]:6.1f}) -> ({j[4][0]:7.1f},{j[4][1]:6.1f}) "
f"scr ({j[5][0]:8.1f},{j[5][1]:7.1f}) -> ({j[6][0]:8.1f},{j[6][1]:7.1f})"
)
# The world anchor's own motion by axis: horizontal is the unit gliding, vertical is the run
# cycle moving the posed head attachment (plus terrain). A standing unit reads 0 in both.
print("\nANCHOR motion — the world point's own Δ per frame, yd (med p90 max):")
for e, s in sorted(rows.items(), key=lambda kv: -len(kv[1]))[:8]:
pairs = [(a_, b) for a_, b in zip(s, s[1:]) if b[0] - a_[0] < GAP_S]
if len(pairs) < 10:
continue
horiz = [
((b[1][0] - a_[1][0]) ** 2 + (b[1][2] - a_[1][2]) ** 2) ** 0.5 for a_, b in pairs
]
vert = [b[1][1] - a_[1][1] for a_, b in pairs]
ys = [r[1][1] for r in s]
print(
f" e={e:<5d} n={len(pairs):<5d} horiz {stat(horiz, '{:7.4f}')} "
f"vert {stat(vert, '{:7.4f}')} vert span {max(ys) - min(ys):.4f} yd"
)
print("\nPROJECTION smoothness — |Δ| and |ΔΔ| of the raw point, per unit (med p90 max):")
for e, s in sorted(rows.items(), key=lambda kv: -len(kv[1]))[:8]:
vis = [r for r in s if on_screen(r[3], vp)]
if len(vis) < 30:
continue
d = [
[b[3][i] - a_[3][i] for i in (0, 1)]
for a_, b in zip(vis, vis[1:])
if b[0] - a_[0] < GAP_S
]
dd = [[b[i] - a_[i] for i in (0, 1)] for a_, b in zip(d, d[1:])]
anchor = max(
(abs(b[1][i] - a_[1][i]) for a_, b in zip(s, s[1:]) for i in (0, 1)), default=0.0
)
print(
f" e={e:<5d} n={len(vis):<5d} anchor {anchor:6.4f} yd/frame "
f"|Δx| {stat([v[0] for v in d])} |ΔΔx| {stat([v[0] for v in dd])} "
f"|ΔΔy| {stat([v[1] for v in dd])}"
)
if __name__ == "__main__":
main()