From abc97bfdcbe62c471e703e533cf8004f4be81994 Mon Sep 17 00:00:00 2001 From: Jennifer Taylor Date: Fri, 6 Nov 2020 16:18:09 +0000 Subject: [PATCH] Several improvements to parsing. Ability to decode graphics regions and assign them to textures. Removed incorrect name handling of one section. Support different endianness of files to support PS3 DanceDanceRevolution AFP files. Fully map out file for several T*BB files. --- bemani/utils/bishiutils.py | 253 +++++++++++++++++++++++-------------- 1 file changed, 157 insertions(+), 96 deletions(-) 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("