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.
78 lines
4.1 KiB
Python
78 lines
4.1 KiB
Python
"""Validate real MASM billiards captures, packed art and tested/shipping parity."""
|
|
import argparse
|
|
import hashlib
|
|
import struct
|
|
from pathlib import Path
|
|
from PIL import Image
|
|
from check_card_exit_build import Coff
|
|
from check_coin_detail import font, text_matches
|
|
from check_terminals import signature
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
OUT = ROOT / "build/actions"
|
|
NAMES = ("billiards-select", "billiards-eight", "billiards-trace", "pyramid-rack",
|
|
"pyramid-aim", "pyramid-strike", "pyramid-penalty", "pyramid-choice", "pyramid-win")
|
|
FUNCTIONS = {"InitPool3D": 0, "ResetPool3D": 0, "StartBilliards3D": 4,
|
|
"PoolStep3D": 0, "PoolCollide3D": 8, "PoolPocketBall3D": 4,
|
|
"PoolJawContact3D": 4, "PoolFinishShot3D": 0, "PoolShoot3D": 0,
|
|
"PoolPredict3D": 0, "PoolDrawPrediction3D": 0,
|
|
"PoolTraceLine3D": 20, "PoolAimPoint3D": 8,
|
|
"PoolChooseAI3D": 0, "EightLegal3D": 4, "EightCount3D": 0,
|
|
"EightAutoCall3D": 0, "EightSyncGroups3D": 0,
|
|
"EightFinish3D": 0, "EightHand3D": 0, "PyramidSelect3D": 4,
|
|
"PyramidRail3D": 8, "PyramidCross3D": 4, "PyramidContact3D": 8,
|
|
"PyramidPenalty3D": 4, "PyramidChoice3D": 4, "PyramidRespot3D": 4,
|
|
"HandlePool3D": 0, "RenderPool3D": 0, "PyramidBall3D": 16}
|
|
|
|
|
|
def main(parity=False):
|
|
for name, count in (("balls-v1.bin", 8), ("pool-balls-v1.bin", 16)):
|
|
raw = (ROOT / "assets/animations/pyramid" / name).read_bytes()
|
|
stride = 12 * 12 * 4
|
|
assert struct.unpack_from("<4s4I", raw) == (b"ANM1", 12, 12, count, stride * count)
|
|
assert len(raw) == 20 + stride * count
|
|
for index in range(count):
|
|
frame = Image.frombytes("RGBA", (12, 12), raw[20 + index * stride:20 + (index + 1) * stride], "raw", "BGRA")
|
|
assert frame.getbbox() is not None
|
|
assert frame.getbbox()[0] > 0 and frame.getbbox()[1] > 0
|
|
frames = {}
|
|
for name in NAMES:
|
|
frame = Image.open(OUT / (name + ".bmp")).convert("RGB")
|
|
assert frame.size == (640, 480)
|
|
assert len(frame.getcolors(640 * 480)) > 300, name
|
|
frames[name] = frame
|
|
frame.save(OUT / (name + ".png"))
|
|
assert len({hashlib.sha256(im.tobytes()).digest() for im in frames.values()}) == len(NAMES)
|
|
glyphs = font()
|
|
text_matches(frames["billiards-select"], "БИЛЬЯРД", 32, 24, 2, (232, 215, 173), glyphs)
|
|
text_matches(frames["billiards-eight"], "ПУЛ / 8", 16, 12, 2, (232, 215, 173), glyphs)
|
|
text_matches(frames["pyramid-rack"], "ПИРАМИДА", 16, 12, 2, (232, 215, 173), glyphs)
|
|
for name, total in (("eight-human-shot", "6"), ("eight-group-credit", "1")):
|
|
frame = Image.open(OUT / (name + ".bmp")).convert("RGB")
|
|
text_matches(frame, "ВЫ", 176, 12, 2, (232, 215, 173), glyphs)
|
|
text_matches(frame, "ИИ", 320, 12, 2, (232, 215, 173), glyphs)
|
|
text_matches(frame, total, 224, 12, 2, (244, 243, 232), glyphs)
|
|
text_matches(frame, total, 288, 12, 2, (244, 243, 232), glyphs)
|
|
frame.save(OUT / (name + ".png"))
|
|
trace = frames["billiards-trace"]
|
|
# The actual two-ball preview must show an object path toward and back from
|
|
# the right cushion, not merely the cue's first-contact ray.
|
|
assert sum(trace.getpixel((x, 200)) == (141, 181, 169) for x in range(350, 570)) > 15
|
|
contact = Image.new("RGB", (1920, 1440))
|
|
for i, name in enumerate(NAMES):
|
|
contact.paste(frames[name], ((i % 3) * 640, (i // 3) * 480))
|
|
contact.save(OUT / "billiards-contact.png")
|
|
if parity:
|
|
tested = Coff(ROOT / "build/billiards/module.obj")
|
|
shipped = Coff(ROOT / "build/graphics/poker.obj")
|
|
for name, size in FUNCTIONS.items():
|
|
symbol = f"_{name}@{size}"
|
|
assert signature(tested,symbol,tail_branches=True) == signature(shipped,symbol,tail_branches=True), name
|
|
print("PASS: Russian selection, both ball sets, native captures, rebound prediction" +
|
|
(" and shipping/tested billiards parity" if parity else ""))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--parity", action="store_true")
|
|
main(parser.parse_args().parity)
|