"""Compile shipping ASM procedures with isolated artifact/tower regression entry.""" import hashlib import json import re from pathlib import Path import struct import subprocess from PIL import Image ROOT = Path(__file__).resolve().parents[2] OUT = ROOT / 'build/artifacts' def toolchain(): for base in (Path('C:/Program Files/Microsoft Visual Studio/18/Community/VC/Tools/MSVC'), Path('C:/Program Files (x86)/Microsoft Visual Studio/18/BuildTools/VC/Tools/MSVC')): if base.exists(): for version in sorted(base.iterdir(), reverse=True): binary = version / 'bin/Hostx64/x86' if (binary / 'ml.exe').exists(): return binary / 'ml.exe', binary / 'link.exe' for base in (Path('F:/MASM32/masm32'), Path('D:/MASM32/masm32'), Path('C:/masm32')): if (base/'bin/ml.exe').exists(): return base/'bin/ml.exe', base/'bin/link.exe' raise RuntimeError('MASM x86 toolchain required') def run(*args): subprocess.run([str(x) for x in args], cwd=ROOT, check=True, timeout=120) def check_text_widths(): source = (ROOT/'src_graphics/hell_tower_data.inc').read_text(encoding='cp1251') for index in range(6): block = source.split(f'towerRelicName{index} db ', 1)[1].split('\ntowerRelicName', 1)[0] block = block.split('\ntowerRelicNames3D', 1)[0] rows = re.findall(r'(?:^|\bdb )([\d,]+)', block) assert len(rows) == 4, (index, rows) for row in rows: data = bytes(map(int, row.split(','))).rstrip(b'\0') assert len(data) <= 11, (index, data.decode('cp1251')) assert 151 not in data for label, width in [('towerRewardTitle3D', 36), ('towerRewardHint3D', 36), ('towerRewardKeys3D', 36), ('towerRunOnly3D', 11)]: row = re.search(rf'^{label} db ([\d,]+)', source, re.M).group(1) data = bytes(map(int, row.split(','))).rstrip(b'\0') assert len(data) <= width and 151 not in data, label def main(): check_text_widths() OUT.mkdir(parents=True, exist_ok=True) runtime = ROOT/'runtime' before = {p: hashlib.sha256(p.read_bytes()).digest() for p in runtime.rglob('*') if p.is_file()} ml, link = toolchain() sdk = Path('C:/Program Files (x86)/Windows Kits/10/Lib') libraries = next(p/'um/x86' for p in sorted(sdk.iterdir(), reverse=True) if (p/'um/x86/kernel32.lib').exists()) run(ml, '/c', '/coff', '/Cp', '/nologo', '/I.', '/DARTIFACT_TEST=1', '/Fobuild/artifacts/test.obj', 'poker.asm') run(link, '/nologo', '/SUBSYSTEM:CONSOLE', '/LIBPATH:'+str(libraries), '/OUT:build/artifacts/test.exe', 'build/artifacts/test.obj', 'kernel32.lib', 'winmm.lib', 'user32.lib', 'gdi32.lib', 'opengl32.lib') run(OUT/'test.exe') assert all(p.exists() and hashlib.sha256(p.read_bytes()).digest() == h for p,h in before.items()) assert {p for p in runtime.rglob('*') if p.is_file()} == set(before), 'test wrote player runtime' profile = (OUT/'profile.dat').read_bytes() assert len(profile) == 312 assert struct.unpack_from('<4s6I', profile, 280)[:6] == (b'ARF2',3,5,1,2,7) assert len((OUT/'legacy.dat').read_bytes()) == 280 for name in ('shop', 'rewards'): image = Image.open(OUT/f'{name}.bmp').convert('RGB') assert image.size == (640,480) and len(image.getcolors(640*480)) > 5 image.save(OUT/f'{name}.png') if name == 'rewards': # Reward scene must not retain any pixels from previously rendered shop. assert image.getpixel((0,0)) == (12,8,11) assert image.getpixel((639,449)) == (12,8,11) assert image.getpixel((639,479)) == (20,9,13) report = {'native': 'PASS', 'profile_bytes': len(profile), 'player_runtime_unchanged': True, 'checks': ['purchases', 'caps', 'toggles', 'observer isolation', 'tower isolation', 'retries', 'reward idempotence', 'prize idempotence', 'bust save', 'shell reveal', 'checksums', 'profile roundtrip', 'legacy import', 'preview roundtrip and tamper rejection', 'disabled effects', 'Buckshot net settlement and idempotence', 'purchase confirmation', 'selection cancels confirmation', 'fresh run full reset', 'native table exits reset tower', 'paid coin exit settles once', 'text widths', 'UI captures']} (OUT/'report.json').write_text(json.dumps(report, indent=2), encoding='utf-8') print('PASS: native artifacts, run-local relics, profile migration and UI captures') if __name__ == '__main__': main()