Add rooftop elevator, lounge, volumetric city, river and animated traffic. Prune invisible mesh faces, clip CPU surfaces, streamline compositing and add automatic/F8 roof quality profiles with renderer diagnostics. Include dartboard/reticle improvements, tracked fortune orb numerals and fate-session visuals, native regression tests, compatible jukebox IDs and updated shipping executables.
177 lines
9.9 KiB
Python
177 lines
9.9 KiB
Python
"""Fresh MASM dart scoring, mouse aiming, full-arm animation and 3D follow-camera proofs."""
|
|
from pathlib import Path
|
|
import argparse
|
|
import hashlib
|
|
import re
|
|
import struct
|
|
import subprocess
|
|
from PIL import Image
|
|
from check_card_exit_build import Coff
|
|
from check_terminals import signature
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
OUT = ROOT / "build/darts"
|
|
|
|
|
|
def read(path):
|
|
raw = (ROOT / path).read_bytes()
|
|
try:
|
|
return raw.decode("utf8").replace("\r\n", "\n")
|
|
except UnicodeDecodeError:
|
|
return raw.decode("cp1251").replace("\r\n", "\n")
|
|
|
|
|
|
def proc(text, name):
|
|
return re.search(rf"^{name} PROC\b.*?^{name} ENDP", text, re.M | re.S).group()
|
|
|
|
|
|
def expand(path):
|
|
return re.sub(r"^include ([^\n]+)$", lambda m: expand(m[1].strip().replace("\\", "/")),
|
|
read(path), flags=re.M)
|
|
|
|
|
|
def prepare():
|
|
OUT.mkdir(parents=True, exist_ok=True)
|
|
code = expand("src_graphics/darts.inc")
|
|
code += "\n" + proc(read("src_graphics/platform_code.inc"), "WindowEvents3D")
|
|
for name in ("ResolveCitySurface3D", "DrawCityVertical3D"):
|
|
code += "\n" + proc(read("src_graphics/city_bins.inc"), name)
|
|
code += "\n" + proc(read("tools/tests/casino_actions_test.inc"), "WriteActionSnapshot3D")
|
|
code += "\nPlayCasinoSound3D PROC arg0:DWORD,arg1:DWORD,arg2:DWORD\n ret\nPlayCasinoSound3D ENDP\n"
|
|
code += "\n" + read("tools/tests/darts_test.inc")
|
|
code += "\n" + read("tools/tests/darts_ui.inc")
|
|
data = expand("src_graphics/darts_data.inc")
|
|
stages = [(0, t) for t in range(0, 320, 40)] + [(1, t) for t in range(0, 200, 40)]
|
|
stages += [(2, t) for t in range(0, 1200, 40)]
|
|
stages += [(3, t) for t in range(0, 1300, 80)] + [(4, t) for t in range(0, 751, 50)]
|
|
code += "\nDartsTestFrames PROC\n call ResetDarts3D\n mov dtAimX3D,0\n mov dtAimY3D,103\n mov dtHitX3D,0\n mov dtHitY3D,103\n"
|
|
for index, (state, time) in enumerate(stages):
|
|
data += f'\n.data\ndtFrame{index} db "build\\darts\\frame-{index:03}.bmp",0\n'
|
|
if state == 3 and stages[index-1][0] != 3:
|
|
code += " mov dtState3D,DT_FLIGHT\n call SettleDarts3D\n"
|
|
code += f" mov dtState3D,{state}\n mov dtTimer3D,{time}\n mov dtClock3D,{time}\n"
|
|
if state == 0:
|
|
code += f" mov dtHolding3D,1\n mov dtHoldTime3D,{time}\n"
|
|
code += f" call RenderDarts3D\n invoke WriteActionSnapshot3D,OFFSET dtFrame{index}\n"
|
|
data += '\n.data\ndtScoreFrame db "build\\darts\\scoreboard.bmp",0\n'
|
|
code += " mov dtScoreOpen3D,1\n call RenderDarts3D\n invoke WriteActionSnapshot3D,OFFSET dtScoreFrame\n"
|
|
code += " call ResetDarts3D\n mov dtHolding3D,1\n mov dtCursorX3D,320*256\n mov dtCursorY3D,214*256-103*DT_FOCAL*256/DT_WALL\n"
|
|
for index in range(80):
|
|
data += f'\n.data\ndtSwayFrame{index} db "build\\darts\\sway-{index:03}.bmp",0\n'
|
|
code += f" mov dtClock3D,{index*40}\n mov dtHoldTime3D,{index*40}\n call DartsAim3D\n call RenderDarts3D\n invoke WriteActionSnapshot3D,OFFSET dtSwayFrame{index}\n"
|
|
code += " ret\nDartsTestFrames ENDP\n"
|
|
code = code.replace(":SDWORD", ":DWORD")
|
|
data += "\n.data\nactionBmpHeader db 42h,4Dh\n dd 1228854\n dw 0,0\n dd 54,40,640,-480\n dw 1,32\n dd 0,1228800,0,0,0,0\n"
|
|
own_funcs = set(re.findall(r"^(\w+) PROC", code, re.M))
|
|
own_data = set(re.findall(r"^(\w+)\s+(?:db|dw|dd|REAL4|REAL8|AnimPack3D|LABEL)\b", data + "\n" + code, re.M))
|
|
source = "\n".join(read(str(p.relative_to(ROOT))) for p in
|
|
[ROOT / "poker.asm", ROOT / "casino3d.inc", ROOT / "casino3d_data.inc",
|
|
*sorted(p for p in (ROOT / "src_graphics").glob("*.inc") if p.name != "renderer.inc")])
|
|
structures = [re.search(rf"^{name} STRUCT\b.*?^{name} ENDS", source, re.M | re.S).group()
|
|
for name in ("AnimPack3D", "BJBot3D")]
|
|
types = {name: kind for name, kind in re.findall(r"^(\w+)\s+(AnimPack3D|BJBot3D)\b", source, re.M)}
|
|
types.update({name: {"db": "BYTE", "dw": "WORD", "dd": "DWORD"}[kind.lower()]
|
|
for name, kind in re.findall(r"^(\w+)\s+(db|dw|dd)\b", source, re.M | re.I)})
|
|
types.update({name: kind.upper() for name, kind in
|
|
re.findall(r"^(\w+)\s+(REAL4|REAL8)\b", source, re.M | re.I)})
|
|
coff = Coff(ROOT / "build/graphics/poker.obj")
|
|
helper = bytearray(coff.data)
|
|
symbol_at = struct.unpack_from("<I", helper, 8)[0]
|
|
externs, protos = [], {}
|
|
for index, name in coff.symbols.items():
|
|
pos = symbol_at + index * 18
|
|
_, section, kind, storage, _ = struct.unpack_from("<IhHBB", helper, pos + 8)
|
|
if section > 0 and re.fullmatch(r"[A-Za-z_]\w*", name) and storage in (2, 3):
|
|
secname = helper[20+(section-1)*40:28+(section-1)*40].rstrip(b"\0")
|
|
if secname in (b".data", b".bss", b".const"):
|
|
helper[pos+16] = 3 if name in own_data else 2
|
|
if name not in own_data:
|
|
externs.append(f"EXTERN {name}:{types.get(name, 'DWORD')}")
|
|
match = re.fullmatch(r"_(\w+)@(\d+)", name)
|
|
if match and kind == 0x20 and section > 0:
|
|
helper[pos+16] = 3 if match[1] in own_funcs else 2
|
|
if match[1] not in own_funcs:
|
|
protos[match[1]] = int(match[2]) // 4
|
|
for name, params in re.findall(r"^(\w+)\s+PROTO([^\n]*)", source, re.M):
|
|
protos.setdefault(name, params.count(":DWORD"))
|
|
for name in own_funcs:
|
|
protos[name] = len(re.findall(r":S?DWORD", proc(code, name).splitlines()[0]))
|
|
constants = dict(re.findall(r"^(\w+)\s+equ\s+([^;\n]+)", source, re.M))
|
|
used = set(re.findall(r"^(\w+) equ", data, re.M))
|
|
ordered = []
|
|
def constant(name):
|
|
if name not in constants or name in used:
|
|
return
|
|
used.add(name)
|
|
for dep in re.findall(r"\b[A-Za-z_]\w*", constants[name]):
|
|
constant(dep)
|
|
ordered.append(f"{name} equ {constants[name]}")
|
|
for name in re.findall(r"\b[A-Za-z_]\w*", code + data):
|
|
constant(name)
|
|
header = ".386\n.model flat\noption casemap:none\n" + "\n".join(structures + ordered + externs)
|
|
header += "\noption language:stdcall\n" + "\n".join(
|
|
name + " PROTO" + (" " + ",".join([":DWORD"]*count) if count else "") for name, count in protos.items())
|
|
data = re.sub(r"^\w+ PROTO[^\n]*\n", "", data, flags=re.M)
|
|
(OUT / "helpers.obj").write_bytes(helper)
|
|
(OUT / "module.asm").write_text(header + "\n" + data + "\n.code\n" + code + "\nend DartsTestStart\n", encoding="cp1251")
|
|
return {name for name in own_funcs if not name.startswith(("DartsTest", "DartsUi"))} - {
|
|
"PlayCasinoSound3D", "WriteActionSnapshot3D"}
|
|
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--parity", action="store_true")
|
|
parser.add_argument("--interactive", action="store_true")
|
|
args = parser.parse_args()
|
|
functions = prepare()
|
|
if args.parity:
|
|
tested, shipping = Coff(OUT / "module.obj"), Coff(ROOT / "build/graphics/poker.obj")
|
|
for name in functions:
|
|
symbol = next(key for key in tested.functions if key.startswith("_" + name + "@"))
|
|
assert signature(tested, symbol, tail_branches=True) == signature(shipping, symbol, tail_branches=True), name
|
|
print("PASS: dart scene, scoring, input and camera match the shipping object")
|
|
return
|
|
masm = Path("F:/MASM32/masm32")
|
|
modern_ml = Path("C:/Program Files/Microsoft Visual Studio/18/Community/VC/Tools/MSVC/14.51.36231/bin/Hostx64/x86/ml.exe")
|
|
assembler = modern_ml if modern_ml.is_file() else masm / "bin/ml.exe"
|
|
subprocess.run([str(assembler), "/c", "/coff", "/Cp", "/nologo", "/I.",
|
|
"/Fobuild/darts/module.obj", "build/darts/module.asm"], cwd=ROOT, check=True)
|
|
sdk = sorted(Path("C:/Program Files (x86)/Windows Kits/10/Lib").glob("*/um/x86"))[-1]
|
|
target = "ui" if args.interactive else "module"
|
|
entry = "DartsUiStart" if args.interactive else "DartsTestStart"
|
|
subprocess.run([str(masm / "bin/link.exe"), "/nologo", "/SUBSYSTEM:WINDOWS", f"/ENTRY:{entry}@0",
|
|
f"/LIBPATH:{sdk}", f"/LIBPATH:{masm/'lib'}", f"/OUT:build/darts/{target}.exe",
|
|
"build/darts/module.obj", "build/darts/helpers.obj", "kernel32.lib", "user32.lib",
|
|
"gdi32.lib", "winmm.lib", "ws2_32.lib"], cwd=ROOT, check=True)
|
|
def profile():
|
|
return {str(p): hashlib.sha256(p.read_bytes()).digest() for p in (ROOT / "runtime").rglob("*")
|
|
if p.is_file()}
|
|
before = profile()
|
|
run = subprocess.run([str(OUT / (target + ".exe"))], cwd=ROOT, timeout=900 if args.interactive else 180)
|
|
assert run.returncode == 0, f"Native darts assertion: {run.returncode}"
|
|
assert profile() == before, "Darts changed persistent profile files"
|
|
if args.interactive:
|
|
print("PASS: interactive native scene closed without changing the profile")
|
|
return
|
|
frames = [Image.open(p).convert("RGB") for p in sorted(OUT.glob("frame-*.bmp"))]
|
|
assert len(frames) >= 60 and len({f.tobytes() for f in frames}) >= 40
|
|
assert all(len(f.getcolors(640*480)) > 80 for f in frames)
|
|
frames[0].save(OUT / "throw.gif", save_all=True, append_images=frames[1:], duration=40, loop=0)
|
|
sheet = Image.new("RGB", (640*3, 480*2))
|
|
for slot, index in enumerate((0, 10, 22, 36, 48, len(frames)-1)):
|
|
sheet.paste(frames[index], ((slot % 3)*640, (slot // 3)*480))
|
|
sheet.save(OUT / "contact.png")
|
|
sway = [Image.open(p).convert("RGB") for p in sorted(OUT.glob("sway-*.bmp"))]
|
|
assert len(sway) == 80 and len({f.tobytes() for f in sway}) > 24
|
|
sway[0].save(OUT / "sway.gif", save_all=True, append_images=sway[1:], duration=40, loop=0)
|
|
triples, = struct.unpack("<I", (OUT / "aim-metrics.bin").read_bytes())
|
|
print(f"PASS: stationary centre-of-T20 aim scored triples in {triples}/100 native samples")
|
|
print("PASS: 67 scoring vectors, 20 complete bot matches, bust/turn isolation, fine mouse/release, sway/fatigue, board footprint, nine-pixel reticle, Tab, orbit camera and native moving frames")
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|
|
|