"""Check grounded waking assets, native dialogue captures and shipping parity.""" import argparse import struct from collections import deque from pathlib import Path from PIL import Image from check_card_exit_build import Coff from check_coin_detail import font, text_matches ROOT = Path(__file__).resolve().parents[2] OUT = ROOT / "build/actions" FUNCTIONS = {"LoadCityArrival3D": 0, "HandleCityApproach3D": 0, "PositionCityCamera3D": 0, "RenderCityArrival3D": 0, "DrawCityWakeActor3D": 0, "DrawCitySeatedLegs3D": 0, "DrawCityLighter3D": 0, "DrawCityWakeSubtitle3D": 0, "BlendCitySky3D": 4, "BlendCityWeatherPixel3D": 16, "DrawCityRain3D": 0, "DrawCitySmokingHand3D": 0, "DrawCityExhaledSmoke3D": 0, "DrawCityCigarette3D": 16, "BeginArrival3D": 0, "HandleArrival3D": 0, "ArrivalEscape3D": 0, "AnimateCityCars3D": 0, "FreeArrivalAssets3D": 0, "RenderCityAlley3D": 0, "ResolveCitySurface3D": 4, "DrawCityVertical3D": 28, "DrawCityHorizontal3D": 24} def check_sleeve(): root = ROOT / "assets/animations/arrival" raw = (root / "wake-sleeve-v1.bin").read_bytes() assert struct.unpack_from("<4I",raw,4) == (256,384,1,256*384*4) sleeve = Image.frombytes("RGBA",(256,384),raw[20:],"raw","BGRA").getchannel("A") assert set(sleeve.tobytes()) == {0,255} assert all(sleeve.crop((0,y,256,y+1)).getbbox() for y in range(384)) anchors = (ROOT / "src_graphics/city_lighter_anchors.inc").read_text() xs, ys = [[int(v) for v in line.split("dd ")[1].split(",")] for line in anchors.splitlines()] lighter = (root / "wake-lighter-v1.bin").read_bytes() for i in range(16): hand = Image.frombytes("RGBA",(256,256),lighter[20+i*262144:20+(i+1)*262144],"raw","BGRA").getchannel("A") mask = Image.new("L",(640,480)) arm = sleeve.resize((220,330),Image.Resampling.NEAREST) mask.paste(arm,(350+xs[i]*220//256-110,223+ys[i]*220//256-20)) hand = hand.resize((220,220),Image.Resampling.NEAREST) mask.paste(255,(350,223),hand) # Start inside the cuff: its connected silhouette must leave the viewport. x,y = 350+xs[i]*220//256,223+ys[i]*220//256-4 queue = deque([(x,y)]) seen = set() reached = False while queue: x,y = queue.popleft() if (x,y) in seen or not (0 <= x < 640 and 0 <= y < 480) or mask.getpixel((x,y)) < 128: continue seen.add((x,y)) if y == 479: reached = True break queue.extend(((x-1,y),(x+1,y),(x,y-1),(x,y+1))) assert reached, f"Detached lighter cuff in frame {i}" def main(parity): check_sleeve() raw = (ROOT / "assets/animations/arrival/wake-rocker-v2.bin").read_bytes() w, h, count, length = struct.unpack_from("<4I", raw, 4) assert (w, h, count, length) == (192,320,32,192*320*4*32) boxes = [] for i in range(count): frame = Image.frombytes("RGBA", (w,h), raw[20+i*w*h*4:20+(i+1)*w*h*4], "raw", "BGRA") box = frame.getbbox() assert box and box[3] == 320, (i, box) assert set(frame.getchannel("A").tobytes()) == {0,255} boxes.append(box) assert max(b[3]-b[1] for b in boxes[:16]) < min(b[3]-b[1] for b in boxes[19:24]) walking = [b[3]-b[1] for b in boxes[24:]] assert max(walking)-min(walking) <= 8, "Walking sprite shrinks between frames" assert abs(max(walking)-(boxes[19][3]-boxes[19][1])) <= 8 wake = Image.open(OUT / "city-wake-rocker.bmp").convert("RGB") text_matches(wake, "Вставай, самурай,",184,422,2,(230,222,210),font()) text_matches(wake, "пора дэпнуть все в казино",120,450,2,(230,222,210),font()) for name in ("city-wake-rocker", "city-rocker-back", "city-light-cigarette", "city-standing", "city-look-right", "city-upward"): frame = Image.open(OUT / (name + ".bmp")).convert("RGB") assert frame.size == (640,480) assert len(frame.getcolors(640*480)) > 500 frame.save(OUT / (name + ".png")) if parity: tested = Coff(OUT / "actions-test.obj") shipped = Coff(ROOT / "build/graphics/poker.obj") for name, size in FUNCTIONS.items(): symbol = f"_{name}@{size}" assert tested.signature(symbol) == shipped.signature(symbol), name back = Image.open(OUT / "city-rocker-back.bmp").convert("RGB") patch = back.crop((250,30,380,180)) pixels = (patch.getpixel((x,y)) for y in range(patch.height) for x in range(patch.width)) assert sum(r > 100 and r > g*1.5 and r > b*1.5 for r,g,b in pixels) > 60, "Back emblem absent" print("PASS: thirty-two grounded poses, back emblem, crouch/standing scale, Russian subtitle, native scenes" + (" and shipping/tested cinematic parity" if parity else "")) if __name__ == "__main__": p = argparse.ArgumentParser() p.add_argument("--parity",action="store_true") main(p.parse_args().parity)