"""Build the verified V21 texture-reclaim runtime DLL from a local game DLL.""" from __future__ import annotations from dataclasses import dataclass from datetime import datetime import hashlib from pathlib import Path import shutil import struct SOURCE_SHA256 = "4b4e5c28e2bfdaa8d36c8341fad27becfd03faf63aa8ea874f5d43c5b1961868" PATCHED_SHA256 = "0be11b06ccf9be747e32b195c8d5980062f446d7d3a0a662ebfe0e602a01c538" TEXT_VA = 0x10001000 TEXT_FILE_OFFSET = 0x400 ENTRY_VA = 0x107C0B40 ENTRY_FILE_OFFSET = TEXT_FILE_OFFSET + (ENTRY_VA - TEXT_VA) ENTRY_ORIGINAL = bytes.fromhex("55 8b ec 6a ff") CAVE_VA = 0x11195CE0 CAVE_FILE_OFFSET = TEXT_FILE_OFFSET + (CAVE_VA - TEXT_VA) TEXT_RAW_SIZE = 0x1194E00 MAX_HOOK_SIZE = TEXT_FILE_OFFSET + TEXT_RAW_SIZE - CAVE_FILE_OFFSET HOOK = bytes.fromhex( "9c60e8490136ff85c0744a89c38d70088dbe505d010039fe743b80be58030000" "00742a8b068b4c242885c974083b81d41f000074188b4c242c85c974083b81d4" "1f000074085089d9e8a32036ff81c650040000ebc1619d5589e56affe904ae62" "ff" ) class TextureReclaimPatchError(RuntimeError): """Raised when the exact V21 binary profile cannot be patched safely.""" @dataclass(frozen=True) class TextureReclaimPatchResult: source: str destination: str source_sha256: str destination_sha256: str entry_va: str cave_va: str hook_size: int old_text_virtual_size: int new_text_virtual_size: int commands: tuple[tuple[str, ...], ...] created_at: str def build_texture_reclaim_patch( source: Path, destination: Path, ) -> TextureReclaimPatchResult: """Create the V21 patch in a separate runtime cache, never in the game tree.""" source = source.expanduser().resolve(strict=True) if _sha256(source) != SOURCE_SHA256: raise TextureReclaimPatchError( "source game.dll does not match the verified V21 profile" ) hook = HOOK commands: list[tuple[str, ...]] = [] if not hook or len(hook) > MAX_HOOK_SIZE: raise TextureReclaimPatchError( f"compiled hook size {len(hook)} exceeds the {MAX_HOOK_SIZE}-byte code cave" ) image = bytearray(source.read_bytes()) if bytes(image[ENTRY_FILE_OFFSET : ENTRY_FILE_OFFSET + len(ENTRY_ORIGINAL)]) != ENTRY_ORIGINAL: raise TextureReclaimPatchError( "V21 map-loader entry bytes do not match the verified profile" ) if any(image[CAVE_FILE_OFFSET : CAVE_FILE_OFFSET + len(hook)]): raise TextureReclaimPatchError("V21 code cave is not empty") text_header = _find_pe_section(image, b".text") old_virtual_size, text_rva, raw_size, raw_offset = struct.unpack_from( " TEXT_RAW_SIZE: raise TextureReclaimPatchError("compiled hook extends beyond the executable section") if old_virtual_size > new_virtual_size: raise TextureReclaimPatchError( "verified executable section is already larger than the patch target" ) entry_displacement = CAVE_VA - (ENTRY_VA + 5) image[ENTRY_FILE_OFFSET : ENTRY_FILE_OFFSET + 5] = b"\xe9" + struct.pack( " int: if len(image) < 0x40: raise TextureReclaimPatchError("source is too small to be a PE image") pe_offset = struct.unpack_from(" str: digest = hashlib.sha256() with path.open("rb") as handle: for chunk in iter(lambda: handle.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest()