1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
|
#!/usr/bin/env python3
"""P43 HUD analysis: prove the new calendar card + energy bar + hotbar are real.
Reads the `LV_P43_SHOT=1` frames and the shot log (which prints the layout rects
computed by HudLayout in the running game), then writes
docs/evidence/p43-analysis.txt with the measured numbers:
1. calendar card / hotbar / slot0 rectangles exactly as the game laid them out,
all inside the 1280x720 viewport and non-overlapping;
2. per-slot hotbar brightness (10 distinct cells) and the selected-slot delta;
3. calendar-card and hotbar opacity is semi-transparent (never an opaque block).
Usage:
python3 scripts/analyze-p43-hud.py <record-dir> <shot-log> <out-txt>
Exit 0 when checks pass, 1 when they fail, 2 on usage/read error.
"""
import re
import sys
from pathlib import Path
import numpy as np
from PIL import Image
WIDTH = 1280
HEIGHT = 720
SHOT_RE = re.compile(
r"p43-shot=(\S+) tick=(\d+) calendar=([\d,]+) hotbar=([\d,]+) slot0=([\d,]+) "
r"selected=(\d+) energy=([\d.]+) day=(\d+) season=(\S+) period=(\S+) clock=(\S+)"
)
def rect(text):
x, y, w, h = (int(v) for v in text.split(","))
return (x, y, w, h)
def inside(name, r):
x, y, w, h = r
return x >= 0 and y >= 0 and x + w <= WIDTH and y + h <= HEIGHT
def main(argv):
if len(argv) != 4:
sys.stderr.write(__doc__)
return 2
record_dir = Path(argv[1])
log_text = Path(argv[2]).read_text(encoding="utf-8")
shots = []
for line in log_text.splitlines():
m = SHOT_RE.search(line)
if m:
shots.append(
{
"name": m.group(1),
"tick": int(m.group(2)),
"calendar": rect(m.group(3)),
"hotbar": rect(m.group(4)),
"slot0": rect(m.group(5)),
"selected": int(m.group(6)),
"energy": float(m.group(7)),
"day": int(m.group(8)),
"season": m.group(9),
"period": m.group(10),
"clock": m.group(11),
}
)
if not shots:
sys.stderr.write("no p43-shot lines in log\n")
return 1
lines = ["P43 HUD analysis (LV_P43_SHOT=1)", ""]
failures = []
for shot in shots:
img = np.asarray(Image.open(record_dir / f"{shot['name']}.png").convert("RGBA"), dtype=np.float64)
lines.append(f"[{shot['name']}] tick={shot['tick']} day={shot['day']} season={shot['season']} "
f"period={shot['period']} clock={shot['clock']} energy={shot['energy']:.0f} selected={shot['selected']}")
cx, cy, cw, ch = shot["calendar"]
bx, by, bw, bh = shot["hotbar"]
sx, sy, sw, sh = shot["slot0"]
lines.append(f" calendar_rect = ({cx},{cy},{cw}x{ch}) hotbar_rect = ({bx},{by},{bw}x{bh}) slot0_rect = ({sx},{sy},{sw}x{sh})")
for name in ("calendar", "hotbar", "slot0"):
if not inside(name, shot[name]):
failures.append(f"{shot['name']} {name} out of viewport {shot[name]}")
# calendar must not overlap the top strip (y >= 39) and must sit left of hotbar band.
if cy < 39:
failures.append(f"{shot['name']} calendar overlaps top bar")
# 1) per-slot brightness across the 10 slots (distinct cells, selected delta).
slot_means = []
for index in range(10):
x = bx + 8 + index * (sw + 6)
y = by + 8
cell = img[y:y + sh, x:x + sw, :3].mean()
slot_means.append(cell)
sel = shot["selected"]
others = [v for i, v in enumerate(slot_means) if i != sel]
lines.append(" hotbar_slot_mean_luma = " + ", ".join(f"{v:.1f}" for v in slot_means))
lines.append(f" selected_slot={sel} selected_luma={slot_means[sel]:.1f} others_mean={np.mean(others):.1f} "
f"delta={slot_means[sel] - np.mean(others):+.1f}")
# 2) opacity: the calendar card and hotbar backdrops must be semi-transparent.
for name, r in (("calendar", shot["calendar"]), ("hotbar", shot["hotbar"])):
x, y, w, h = r
alpha = img[y:y + h, x:x + w, 3]
region = img[y:y + h, x:x + w, :3]
opaque_white = int(((alpha >= 250) & (region.min(axis=2) >= 250)).sum())
lines.append(f" {name}: alpha_min={int(alpha.min())} alpha_max={int(alpha.max())} "
f"mean_luma={region.mean():.1f} opaque_white_px={opaque_white}")
if opaque_white > 0:
failures.append(f"{shot['name']} {name} contains opaque white pixels")
if region.mean() < 8.0:
failures.append(f"{shot['name']} {name} too dark (possible big black block)")
lines.append("")
lines.append("verdict = " + ("PASS" if not failures else "FAIL"))
out_txt = Path(argv[3])
out_txt.parent.mkdir(parents=True, exist_ok=True)
out_txt.write_text("\n".join(lines) + "\n", encoding="utf-8")
sys.stdout.write("\n".join(lines) + "\n")
if failures:
for f in failures:
sys.stderr.write(f"FAIL {f}\n")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv))
|