#!/usr/bin/env python3 """Compile and run real C++/ASM regression scenarios in an isolated TEMP tree. Requires the same MASM32 and MSVC tools as the normal builds. No test opens the editor/game window or writes the project's levels, textures or executables. Use --keep to retain the generated framebuffer and fixtures for inspection. """ import argparse import hashlib import importlib.util import math import os from pathlib import Path import re import shutil import struct import subprocess import sys import tempfile import wave ROOT = Path(__file__).resolve().parents[1] def run(args, cwd): result = subprocess.run(args, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120, errors="replace") if result.returncode: print(result.stdout) raise RuntimeError(f"Failed ({result.returncode}): {args[0]}") return result.stdout def fixtures(work): assets = work / "assets" assets.mkdir() (work / "editor").mkdir() grid = bytearray(1024) for x in range(32): grid[x] = grid[31 * 32 + x] = 1 for y in range(32): grid[y * 32] = grid[y * 32 + 31] = 1 def level(name, *, version=3, count=2, transitions=None, black=False, spawn=(2, 2), cells=None): cells = bytearray(grid if cells is None else cells) transitions = [(4, 2, b"sublevel_02.dat", 3, 2, 0, 1, 0)] if transitions is None else transitions for t in transitions: cells[t[1]*32+t[0]] = 10 h = struct.pack("nul 2>&1\n' batch += f'cl {flags} "{ROOT / "tests/playtest_stub.cpp"}" /Fe:engine.exe\nif errorlevel 1 exit /b 1\n' batch += f'cl {flags} ' + " ".join(f'"{p}"' for p in sources) batch += ' /Fe:editor_tests.exe /link /SUBSYSTEM:CONSOLE d3d11.lib dxgi.lib user32.lib gdi32.lib shell32.lib comdlg32.lib winmm.lib\n' (work / "build_tests.bat").write_text(batch) run(["cmd", "/c", str(work / "build_tests.bat")], work) print(run([str(work / "editor_tests.exe"), str(ROOT / "assets"), str(ROOT / "engine.exe")], work / "editor"), end="") spec = importlib.util.spec_from_file_location("texturepack", ROOT / "tools/make_texturepack.py") texturepack = importlib.util.module_from_spec(spec) spec.loader.exec_module(texturepack) generated = b"".join(texturepack.GENERATORS[i]() for i in range(16)) if generated != (work / "assets/defaults.bin").read_bytes(): raise RuntimeError("Python texture generator differs from editor/runtime defaults") print("PASS Python, editor and engine use identical procedural texture defaults") main_source = (ROOT / "main.asm").read_text(encoding="utf-8") api = main_source[:main_source.index("; ------------------------------------------------------------------------------\n; Engine Includes")] api = re.sub(r"^(GetAsyncKeyState|GetForegroundWindow|PlaySoundA)\s+PROTO.*\n", "", api, flags=re.M) (work / "engine_api.inc").write_text(api) start = main_source.index("ParseStartupMap PROC") end = main_source.index("ParseStartupMap ENDP", start) + len("ParseStartupMap ENDP") (work / "startup_parser.inc").write_text(main_source[start:end]) (work / "project_paths.inc").write_text( f'.data\nprojectMapPath db "{ROOT / "assets/map.dat"}",0\n' f'projectPackPath db "{ROOT / "assets/textures/texturepack.bin"}",0\n.code\n') masm_candidates = [Path(p) for p in ("D:/MASM32/masm32/bin/ml.exe", "C:/masm32/bin/ml.exe", "F:/MASM32/masm32/bin/ml.exe")] masm_candidates += list(Path("C:/Program Files (x86)/Microsoft Visual Studio").glob("*/BuildTools/VC/Tools/MSVC/*/bin/Hostx64/x86/ml.exe")) ml = next((p for p in masm_candidates if p.is_file()), None) if not ml: raise RuntimeError("x86 MASM assembler was not found") sdk = sorted(Path("C:/Program Files (x86)/Windows Kits/10/Lib").glob("*/um/x86")) if not sdk: raise RuntimeError("Windows x86 SDK libraries were not found") run([str(ml), "/c", "/coff", "/Cp", "/nologo", f"/I{work}", f"/I{ROOT}", f"/Fo{work / 'engine_tests.obj'}", str(ROOT / "tests/engine_regressions.asm")], work) link = ml.with_name("link.exe") run([str(link), "/NOLOGO", "/SUBSYSTEM:CONSOLE", f"/LIBPATH:{sdk[-1]}", f"/OUT:{work / 'engine_tests.exe'}", str(work / "engine_tests.obj"), "kernel32.lib", "user32.lib", "gdi32.lib", "winmm.lib"], work) run([str(work / "engine_tests.exe"), "pace map.dat"], work) source = (ROOT / "tests/engine_regressions.asm").read_text() + (ROOT / "tests/cutscene_engine_checks.inc").read_text() names = re.findall(r"^\s+CHECK .*?; (.+)$", source, re.M) raw = (work / "engine_results.bin").read_bytes() values = struct.unpack(f"<{len(raw)//4}I", raw) if values[0] != len(names): raise RuntimeError(f"ASM result count mismatch: {values[0]} vs {len(names)}") for name, passed in zip(names, values[1:]): print(("PASS " if passed else "FAIL ") + name) angles = struct.unpack("<320i", (work / "angles.bin").read_bytes()) expected = tuple(round(math.atan((x-160)/160*math.tan(48*math.pi/256))*256/math.pi) for x in range(320)) if angles != expected or not all(values[1:]): raise RuntimeError("ASM regressions failed") print(f"ASM: {len(names)}/{len(names)} passed; 320 ray angles match the specification") frame = (work / "frame.bgra").read_bytes() bitmap_header = struct.pack("<2sIHHI", b"BM", len(frame)+54, 0, 0, 54) bitmap_header += struct.pack("