"""Offline ABI verification against a mapped/decrypted image, never an on-disk encrypted EXE. Usage: python3 tests/verify_ember_movie_native.py path/to/game_image.bin [packages-directory] """ import re import struct import sys from pathlib import Path repo = Path(__file__).resolve().parents[1] data = Path(sys.argv[1]).read_bytes() def signature(file, name): source = (repo / file).read_text() pattern = re.search(r'constexpr auto ' + name + r'\s*=\s*signature skip GPU upload # Captured a47adbd exception: C0000005, RIP 1204163, RAX=0, RBX=80BCA021, # RDX=4. The registration callback reads the unloaded definition's slot directly. assert data[0x1204160:0x1204167] == bytes.fromhex('48 2b c1 48 0f be 08') # Native type-19 definitions need 16 bytes, not their eight-byte package size. assert struct.unpack_from(' window enum -> authored window name mapping. def fnv1(name): value=0x811C9DC5 for byte in name.encode(): value=((value*0x1000193)&0xFFFFFFFF)^byte return value for state, enum, branch, name in [(0x21,26,0x13125D0,'cinematic_overlay'), (0x22,29,0x131258E,'loading')]: assert struct.unpack_from(' cinematic_overlay; 22h -> loading verified; UI layers preserved.') # A playback overlay does not remove the separate gameplay HUD. Filter only # its full-window submission, before 13D9060 can replay the cached subtree. hud = 'Sunrise/src/client/hooks/bootflow/ember_movie_hud.cpp' window_call = signature(hud, 'windowDrawSig') widget_draw = signature(hud, 'widgetSig') assert window_call == 0x132C1B2 and widget_draw == 0x13D9060 target(window_call, 11, widget_draw) assert window_call + 16 == 0x132C1C2 # The caller passes the full window at intrusive-list node minus 600h. assert data[0x132C0F9:0x132C100] == bytes.fromhex('48 8d b3 00 fa ff ff') assert data[0x132C1BA:0x132C1BD] == bytes.fromhex('48 8b ce') # Child submissions share this callee but have a different caller and only a # widget-sized allocation. They must pass through without accessing +310. target(0x13D91FA, 0, widget_draw) # Native HUD creation selects "hud" (or an equipment override), then assigns # the semantic role 18. The role setter writes window+310, not state enum +410. assert struct.unpack_from(' kind 1; shared type-16 tag (type_info & F000 == 2000) -> kind 2. assert data[0x42694F:0x42696F] == bytes.fromhex( '8b 45 04 8b cb 48 89 7c 24 30 25 00 f0 00 00 33 ff 3d 00 20 00 00 40 0f 94 c7 45 33 c0 8d 57 01') target(0x426920, 0x4F, 0x433050) # Kind 2 is routed to root+10; ordinary metadata belongs in root+20. assert data[0x4313CD:0x4313E2] == bytes.fromhex( '83 3f 02 b9 10 00 00 00 8b 57 04 41 b8 20 00 00 00 44 0f 44 c1') # For stream type_info & 30000 == 10000, the load job maps offset|patch and # size|C0000000 directly. It bypasses the ordinary allocation/read branch. assert data[0x3592C6:0x3592EB] == bytes.fromhex( '8b c3 c1 e8 10 83 e0 03 83 f8 01 75 2f 41 0f b7 4d 20 41 81 cf 00 00 00 c0 8b 55 50 45 8b c7 48 0b d1 8b 4d 48') target(0x3591B0, 0x13B, 0x351D00) target(0x41A160, 0x16, 0x3597C0) # native video I/O opens this mapped package/patch target(0x41A160, 0x2C, 0x357DA0) # then obtains offset and byte length movie = 'Sunrise/src/client/hooks/ember_movies/ember_movies.cpp' start, stop, busy = (signature(movie, name) for name in ('startSig', 'stopSig', 'busySig')) for offset, expected in [(0x72, 0x41B040), (0x7A, 0x41A3C0), (0x8E, 0x41CD20)]: target(start, offset, expected) for offset, expected in [(0x18, 0x41D0C0), (0x25, 0x41A980)]: target(stop, offset, expected) assert busy == 0x41B420 target(busy, 0x48, 0x41AB70) if len(sys.argv) > 2: # Read only container metadata, without unpacking data or starting the game. latest = {} for path in Path(sys.argv[2]).glob('*.pkg'): with path.open('rb') as stream: header = stream.read(0x170) package = struct.unpack_from(' latest[package][0]: latest[package] = version, path, header for tag, expected in [(0x80BCA001, 0x80808495), (0x80BCA003, 0x80808495), (0x80BCA000, 0x80808499), (0x80BCA002, 0x80808499), (0x80B9EB33, 0x80809A88), (0x80B9EB34, 0x80809A88), (0x80BCA032, 0x80806B8F), (0x80BCA022, 0x80806B91), (0x80BCA025, 0x80806B91), (0x80BCA028, 0x80806B91), (0x80BCA02B, 0x80806B91), (0x80BCA02E, 0x80806B91), (0x80BCA031, 0x80806B91), (0x80BCA021, 0x80BCA020), (0x80BCA024, 0x80BCA023), (0x80BCA026, 0x80BCA027), (0x80BCA029, 0x80BCA02A), (0x80BCA02C, 0x80BCA02D), (0x80BCA02F, 0x80BCA030), (0x80BCA020, 0x80BCA021), (0x80BCA023, 0x80BCA024), (0x80BCA027, 0x80BCA026), (0x80BCA02A, 0x80BCA029), (0x80BCA02D, 0x80BCA02C), (0x80BCA030, 0x80BCA02F), (0x80BCA034, 0xFFFFFFFF), (0x80C7C000, 0xFFFFFFFF)]: # Tag package IDs include the bank: 80BCAxxx belongs to package 01E5. package = (tag >> 13) & 0x3FF _, path, header = latest[package] table = (struct.unpack_from('> 6) & 0x3F == 24 if 0x80BCA020 <= expected <= 0x80BCA030: definitions = {0x80BCA021,0x80BCA024,0x80BCA026,0x80BCA029,0x80BCA02C,0x80BCA02F} assert (type_info & 0x3FFFF) == (0x44FB if tag in definitions else 0x254FB) print('Movie metadata, streams, surface containers, definitions and raw buffers all use kind 1.') tags = repo / 'build/first-encounter-audit/tags' catalog = (tags / '80BCA032.bin').read_bytes() for i, (container, definition) in enumerate(zip( [0x80BCA022,0x80BCA025,0x80BCA028,0x80BCA02B,0x80BCA02E,0x80BCA031], [0x80BCA021,0x80BCA024,0x80BCA026,0x80BCA029,0x80BCA02C,0x80BCA02F])): assert struct.unpack_from('