KAZIK/tools/tests/check_artifacts.py
Игорь Федосеев eaac8b861e feat: rework artifacts and tower relics
Extend profiles from 280 to 312 bytes with legacy compatibility.
Keep tower relics run-local and share the profile bankroll.
2026-10-30 23:22:19 +03:00

95 lines
4.7 KiB
Python

"""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()