Extend profiles from 280 to 312 bytes with legacy compatibility. Keep tower relics run-local and share the profile bankroll.
95 lines
4.7 KiB
Python
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()
|