- Native player-controlled stairs, upper lounge bar, and Hell Tower infernal portal - Cabaret stage with musical acts, audience seating, and interaction - Dedicated cash/trophy wall terminals with native balance and achievement screens - Projecting 3D wall lanterns with ray/plane intersection - Six roaming guests with seated poses and card rank rendering updates - Synchronized launcher hash and updated binaries
146 lines
6 KiB
Python
146 lines
6 KiB
Python
"""Compile the actual MASM card renderers and check every visible rank."""
|
|
import argparse
|
|
from pathlib import Path
|
|
import re
|
|
import struct
|
|
import subprocess
|
|
|
|
from PIL import Image
|
|
|
|
from check_card_exit_build import Coff
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
OUT = ROOT / "build/card-ranks"
|
|
RANKS = ("2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A")
|
|
FUNCTIONS = {
|
|
"casino3d.inc": ("DrawPixel", "DrawBox", "DrawBoxBorder", "DrawChar8x8", "DrawString8x8"),
|
|
"src_graphics/card_table_assets.inc": ("DrawNativeSuit3D", "DrawNativeCard3D"),
|
|
"src_graphics/native_blackjack_party_render.inc": ("DrawBJMiniCard3D",),
|
|
}
|
|
|
|
|
|
def read(path):
|
|
return (ROOT / path).read_text(encoding="cp1251")
|
|
|
|
|
|
def procedure(source, name):
|
|
return re.search(rf"^{name} PROC\b.*?^{name} ENDP", source, re.M | re.S).group()
|
|
|
|
|
|
def prepare():
|
|
OUT.mkdir(exist_ok=True)
|
|
ranks = re.search(r"^cardRankNames3D db .*", read("src_graphics/card_tables_data.inc"), re.M).group()
|
|
suits = re.search(r"^cardSuitMasks3D db .*\n(?: db .*\n){3}", read("src_graphics/card_tables_data.inc"), re.M).group()
|
|
backs = re.search(r"^cardBackColors3D dd .*\n dd .*", read("src_graphics/platform_data.inc"), re.M).group()
|
|
parts = [""".386
|
|
.model flat,stdcall
|
|
option casemap:none
|
|
SCREEN_W equ 640
|
|
SCREEN_H equ 480
|
|
ExitProcess PROTO :DWORD
|
|
CreateFileA PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD
|
|
WriteFile PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD
|
|
CloseHandle PROTO :DWORD
|
|
DrawBox PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD
|
|
DrawAnimSprite3D PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD
|
|
AnimPack3D STRUCT
|
|
magic dd ?
|
|
imageWidth dd ?
|
|
height dd ?
|
|
frames dd ?
|
|
bytes dd ?
|
|
pixels dd ?
|
|
AnimPack3D ENDS
|
|
.data
|
|
include font8x8.inc
|
|
selectedBack3D dd 0
|
|
backsPack3D AnimPack3D <>
|
|
actionBmpHeader db 42h,4Dh
|
|
dd 1228854
|
|
dw 0,0
|
|
dd 54,40,640,-480
|
|
dw 1,32
|
|
dd 0,1228800,0,0,0,0
|
|
""", ranks, suits, backs]
|
|
for suit in range(4):
|
|
for page in range(2):
|
|
parts.append(f'sheet{suit}_{page} db "build/card-ranks/suit-{suit}-{page}.bmp",0')
|
|
parts.append(".data?\nscreenBuffer dd 640*480 dup(?)\n.code")
|
|
for path, names in FUNCTIONS.items():
|
|
source = read(path)
|
|
parts.extend(procedure(source, name) for name in names)
|
|
parts.append(procedure(read("tools/tests/casino_actions_test.inc"), "WriteActionSnapshot3D"))
|
|
parts.append("""DrawAnimSprite3D PROC pack:DWORD,frame:DWORD,x:DWORD,y:DWORD,w:DWORD,h:DWORD
|
|
invoke ExitProcess,99
|
|
ret
|
|
DrawAnimSprite3D ENDP
|
|
CardRanksStart PROC
|
|
""")
|
|
for suit in range(4):
|
|
for page in range(2):
|
|
parts.append("invoke DrawBox,0,0,320,240,00213629h")
|
|
for rank in range(page * 7, min(page * 7 + 7, 13)):
|
|
x = 6 + (rank % 7) * 44
|
|
for mode, width in enumerate((34, 20, 18, 20)):
|
|
y = 4 + mode * 60
|
|
if mode == 3:
|
|
parts.append(f"invoke DrawBJMiniCard3D,{x},{y},{suit*13+rank},0")
|
|
else:
|
|
parts.append(f"invoke DrawNativeCard3D,{x},{y},{suit*13+rank},0,{width}")
|
|
parts.append(f"invoke WriteActionSnapshot3D,OFFSET sheet{suit}_{page}\ntest eax,eax\njz failed")
|
|
parts.append("invoke ExitProcess,0\nfailed:\ninvoke ExitProcess,1\nCardRanksStart ENDP\nend CardRanksStart")
|
|
(OUT / "ranks.asm").write_text("\n".join(parts), encoding="ascii")
|
|
|
|
|
|
def verify():
|
|
font = bytes(int(x, 16) for x in re.findall(r"\b([0-9A-Fa-f]{3})h\b", read("font8x8.inc")))
|
|
assert len(font) == 256 * 8
|
|
for suit in range(4):
|
|
ink = (191, 38, 60) if suit in (1, 2) else (25, 31, 36)
|
|
for page in range(2):
|
|
image = Image.open(OUT / f"suit-{suit}-{page}.bmp").convert("RGB")
|
|
for rank in range(page * 7, min(page * 7 + 7, 13)):
|
|
x = 6 + (rank % 7) * 44
|
|
for mode, width in enumerate((34, 20, 18, 20)):
|
|
y = 4 + mode * 60
|
|
offset = 2 if mode == 3 else (1 if width == 18 else 3)
|
|
expected = {(x + offset + letter * 8 + col, y + 3 + row)
|
|
for letter, char in enumerate(RANKS[rank])
|
|
for row in range(8) for col in range(8)
|
|
if font[ord(char) * 8 + row] & (128 >> col)}
|
|
actual = {(px, py) for px in range(x, x + width)
|
|
for py in range(y + 3, y + 11)
|
|
if image.getpixel((px * 2, py * 2)) == ink}
|
|
assert actual == expected, (suit, rank, mode, actual ^ expected)
|
|
assert all(x < px < x + width - 1 for px, _ in expected)
|
|
image.save(OUT / f"suit-{suit}-{page}.png")
|
|
print("PASS: 208 native card faces; 10/A and all ranks, four suits, narrow/normal/guest cards")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--parity", action="store_true")
|
|
args = parser.parse_args()
|
|
if args.parity:
|
|
tested, shipped = Coff(OUT / "ranks.obj"), Coff(ROOT / "build/graphics/poker.obj")
|
|
for name, count in (("DrawNativeCard3D", 20), ("DrawBJMiniCard3D", 16)):
|
|
symbol = f"_{name}@{count}"
|
|
assert tested.signature(symbol) == shipped.signature(symbol), name
|
|
table = b"".join(rank.encode().ljust(3, b"\0") for rank in RANKS)
|
|
assert table in tested.data and table in shipped.data
|
|
verify()
|
|
print("PASS: shipping card renderers and rank strings match the tested MASM")
|
|
return
|
|
prepare()
|
|
masm = Path("F:/MASM32/masm32")
|
|
subprocess.run([str(masm / "bin/ml.exe"), "/c", "/coff", "/Cp", "/nologo", "/I.",
|
|
"/Fobuild/card-ranks/ranks.obj", "build/card-ranks/ranks.asm"], cwd=ROOT, check=True)
|
|
subprocess.run([str(masm / "bin/link.exe"), "/nologo", "/SUBSYSTEM:CONSOLE",
|
|
f"/LIBPATH:{masm / 'lib'}", "/OUT:build/card-ranks/ranks.exe",
|
|
"build/card-ranks/ranks.obj", "kernel32.lib"], cwd=ROOT, check=True)
|
|
subprocess.run([str(OUT / "ranks.exe")], cwd=ROOT, check=True)
|
|
verify()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|