diff --git a/bemani/utils/bishiutils.py b/bemani/utils/bishiutils.py index 46a0aff..ee02bec 100644 --- a/bemani/utils/bishiutils.py +++ b/bemani/utils/bishiutils.py @@ -4,8 +4,9 @@ import os import os.path import struct import sys +import textwrap from PIL import Image, ImageOps # type: ignore -from typing import Any, List +from typing import Any, List, Optional from bemani.format.dxt import DXTBuffer from bemani.protocol.binary import BinaryEncoding @@ -64,19 +65,21 @@ def descramble_text(text: bytes, obfuscated: bool) -> str: return "" -def descramble_pman(package_data: bytes, offset: int, obfuscated: bool) -> List[str]: +def descramble_pman(package_data: bytes, offset: int, endian: str, obfuscated: bool) -> List[str]: # Unclear what the first three unknowns are, but the fourth # looks like it could possibly be two int16s indicating unknown? magic, _, _, _, numentries, _, data_offset = struct.unpack( - "<4sIIIIII", + f"{endian}4sIIIIII", package_data[offset:(offset + 28)], ) add_coverage(offset, 28) - if magic != b"PMAN": + if endian == "<" and magic != b"PMAN": + raise Exception("Invalid magic value in PMAN structure!") + if endian == ">" and magic != b"NAMP": raise Exception("Invalid magic value in PMAN structure!") - names = [] + names: List[Optional[str]] = [None] * numentries if numentries > 0: # Jump to the offset, parse it out for i in range(numentries): @@ -84,7 +87,7 @@ def descramble_pman(package_data: bytes, offset: int, obfuscated: bool) -> List[ # Really not sure on the first entry here, it looks # completely random, so it might be a CRC? _, entry_no, nameoffset = struct.unpack( - " List[ bytedata = get_until_null(package_data, nameoffset) add_coverage(nameoffset, len(bytedata) + 1, unique=False) name = descramble_text(bytedata, obfuscated) - names.append(name) + names[entry_no] = name + + for i, name in enumerate(names): + if name is None: + raise Exception(f"Didn't get mapping for entry {i + 1}") return names -def swap32(i: int) -> int: - return struct.unpack("I", i))[0] - - def extract(filename: str, output_dir: str, *, write: bool, verbose: bool = False) -> None: with open(filename, "rb") as fp: data = fp.read() @@ -121,7 +124,11 @@ def extract(filename: str, output_dir: str, *, write: bool, verbose: bool = Fals # First, check the signature add_coverage(0, 4) - if data[0:4] != b"2PXT": + if data[0:4] == b"2PXT": + endian = "<" + elif data[0:4] == b"TXP2": + endian = ">" + else: raise Exception("Invalid graphic file format!") # Not sure what words 2 and 3 are, they seem to be some sort of @@ -130,19 +137,19 @@ def extract(filename: str, output_dir: str, *, write: bool, verbose: bool = Fals # Now, grab the file length, verify that we have the right amount # of data. - length = struct.unpack("" and magic != b"TXDT": raise Exception("Unexpected texture format!") - img = None if fmt == 0x0E: # RGB image, no alpha. img = Image.frombytes( 'RGB', (width, height), raw_data[64:], 'raw', 'RGB', ) # 0x10 = Seems to be some sort of RGB with color swapping. + # 0x11 = Unknown entirely, PS3 format. Looks to be one byte per pixel. # 0x15 = Looks like RGB but reversed (end and beginning bytes swapped). # 0x16 = DTX1 format, when I encounter this I'll hook it up. elif fmt == 0x1A: @@ -261,15 +270,26 @@ def extract(filename: str, output_dir: str, *, write: bool, verbose: bool = Fals 'RGBA', (width, height), raw_data[64:], 'raw', 'BGRA', ) else: - raise Exception(f"Unsupported format {hex(fmt)} for texture {name}") + print(f"Unsupported format {hex(fmt)} for texture {name}") + img = None # Actually place the file down. os.makedirs(path, exist_ok=True) - with open(f"{filename}.raw", "wb") as bfp: - bfp.write(raw_data) if img: with open(f"{filename}.png", "wb") as bfp: img.save(bfp, format='PNG') + else: + with open(f"{filename}.raw", "wb") as bfp: + bfp.write(raw_data) + with open(f"{filename}.xml", "w") as sfp: + sfp.write(textwrap.dedent(f""" + + {width} + {height} + {hex(fmt)} + {filename}.raw + + """).strip()) vprint(f"Bit 0x000001 - count: {length}, offset: {hex(offset)}") for name in names: @@ -277,20 +297,21 @@ def extract(filename: str, output_dir: str, *, write: bool, verbose: bool = Fals else: vprint("Bit 0x000001 - NOT PRESENT") + # Mapping between texture index and the name of the texture. + texturemap = [] if feature_mask & 0x02: - # Seems to be a structure that duplicates texture names? Maybe this is - # used elsewhere to map sections to textures? The structure includes - # the entry number that seems to correspond with the above table. - offset = struct.unpack(" 0: + texture_to_region = [(0, (0, 0), (0, 0))] * length + + for i in range(length): + descriptor_offset = offset + (10 * i) + texture_no, left, top, right, bottom = struct.unpack( + f"{endian}HHHHH", + data[descriptor_offset:(descriptor_offset + 10)], + ) + add_coverage(descriptor_offset, 10) + + if texture_no < 0 or texture_no >= len(texturemap): + raise Exception(f"Out of bounds texture {texture_no}") + + # TODO: The offsets here seem to be off by a power of 2, there + # might be more flags in the above texture format that specify + # device scaling and such? + texture_to_region[i] = (texture_no, (left, top), (right, bottom)) + vprint(f"Bit 0x000008 - count: {length}, offset: {hex(offset)}") else: vprint("Bit 0x000008 - NOT PRESENT") if feature_mask & 0x10: - # Seems to be a strucure that duplicates the above section? - offset = struct.unpack("= len(texture_to_region): + raise Exception(f"Out of bounds region {i}") + region = texture_to_region[i] + texture = texturemap[region[0]] + + filename = os.path.join(path, name) + if write: + # Actually place the file down. + os.makedirs(path, exist_ok=True) + + print(f"Writing {filename}.xml graphic information...") + with open(f"{filename}.xml", "w") as sfp: + sfp.write(textwrap.dedent(f""" + + {region[1][0]} + {region[1][1]} + {region[2][0]} + {region[2][1]} + {texture} + + """).strip()) + else: + print(f"Would write {filename}.xml graphic information...") + + vprint(f" {i}: {name}") else: vprint("Bit 0x000010 - NOT PRESENT") @@ -333,28 +402,13 @@ def extract(filename: str, output_dir: str, *, write: bool, verbose: bool = Fals if feature_mask & 0x40: # Two unknown bytes, first is a length or a count. Secound is # an optional offset to grab another set of bytes from. - length, offset = struct.unpack(" 0, we use the magic flag # from above in this case to optionally transform each thing we - # extract. + # extract. This is possibly names of some other type of struture? else: vprint("Bit 0x000100 - NOT PRESENT") if feature_mask & 0x200: # One unknown byte, treated as an offset. - offset = struct.unpack("