"""Validate generated packs and snapshots from the actual MASM renderer.""" import hashlib import argparse import json import struct from pathlib import Path from PIL import Image, ImageDraw ROOT = Path(__file__).resolve().parents[2] OUTPUT = ROOT / 'build/actions' def main(packs_only=False): manifest = json.loads((ROOT / 'assets/animations/buckshot/manifest-v2.json').read_text()) for name, expected in manifest.items(): data = (ROOT / 'assets/animations/buckshot' / name).read_bytes() magic, width, height, count, length = struct.unpack_from('<4s4I', data) assert magic == b'ANM1' assert [width, height] == expected['size'] and count == expected['frames'] assert len(data) == 20 + length == 20 + width * height * 4 * count hashes = set() for i in range(count): payload = data[20 + i * width * height * 4:20 + (i + 1) * width * height * 4] frame = Image.frombytes('RGBA', (width, height), payload, 'raw', 'BGRA') hashes.add(hashlib.sha256(payload).hexdigest()) if name == 'self-flash-v4.bin' and i < 4: hot = [(x, y, a) for y in range(height) for x in range(width) for r, g, b, a in [frame.getpixel((x, y))] if r > 240 and g > 220 and b > 150 and a > 64] weight = sum(a for x, y, a in hot) assert weight > 0, (name, i) cx = sum(x * a for x, y, a in hot) / weight cy = sum(y * a for x, y, a in hot) / weight assert abs(cx - 64) < 2 and abs(cy - 64) < 2, (name, i, cx, cy) if name not in ('room-v2.bin', 'blood-v3.bin', 'self-flash-v4.bin'): assert set(frame.getchannel('A').tobytes()) == {0, 255}, (name, i) bounds = frame.getchannel('A').getbbox() assert bounds and 0 < bounds[0] < bounds[2] < width, (name, i, bounds) assert len(hashes) == count, name if packs_only: print(f'PASS: {len(manifest)} packs, frontal ignition anchors centered within 2 pixels') return snapshots = sorted(OUTPUT.glob('buckshot-*.bmp')) assert len(snapshots) == 30, len(snapshots) report = {} for path in snapshots: frame = Image.open(path).convert('RGB') assert frame.size == (640, 480) colors = len(frame.getcolors(640 * 480)) assert colors > 1000, (path.name, colors) cinematic = any(part in path.stem for part in ('revive-', 'death-')) or path.stem in ( 'buckshot-self-fire', 'buckshot-self-peak', 'buckshot-self-smoke', 'buckshot-dealer-fire') if not cinematic: assert frame.getpixel((0, 0)) == (17, 17, 22), path.name assert frame.getpixel((0, 479)) == (17, 22, 23), path.name else: assert frame.getpixel((0, 479)) != (17, 22, 23), ('HUD hides cinematic', path.name) report[path.stem] = {'colors': colors, 'sha256': hashlib.sha256(frame.tobytes()).hexdigest()} frame.save(path.with_suffix('.png')) assert len({item['sha256'] for item in report.values()}) == len(snapshots) peak = Image.open(OUTPUT / 'buckshot-self-peak.bmp').convert('RGB') r, g, b = peak.getpixel((315, 228)) assert r > 240 and g > 220 and b > 150, ('Muzzle ignition missed the bore', r, g, b) names = ['ready', 'self-peak', 'death-dealer', 'revive-dealer-shock'] contact = Image.new('RGB', (1280, 1010), '#191c19') draw = ImageDraw.Draw(contact) for i, name in enumerate(names): x, y = i % 2 * 640, i // 2 * 505 draw.text((x + 8, y + 5), name, fill='white') contact.paste(Image.open(OUTPUT / f'buckshot-{name}.bmp'), (x, y + 25)) contact.save(OUTPUT / 'buckshot-contact-sheet.png') sequence = ['self-rise', 'self-aim', 'self-fire', 'self-peak', 'self-smoke', 'death-player', 'revive-player-arms', 'revive-player-shock', 'lose'] frames = [Image.open(OUTPUT / f'buckshot-{name}.bmp').convert('RGB') for name in sequence] frames[0].save(OUTPUT / 'buckshot-keyframes.gif', save_all=True, append_images=frames[1:], duration=[300, 300, 30, 50, 300, 800, 700, 800, 1000], loop=0) (OUTPUT / 'buckshot-qa.json').write_text(json.dumps(report, indent=2), encoding='utf-8') print(f'PASS: {len(manifest)} packs, 148 distinct sprite/VFX poses, 30 MASM framebuffer snapshots') if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--packs-only', action='store_true') main(parser.parse_args().packs_only)