diff --git a/bemani/format/afp.py b/bemani/format/afp.py index 3617e64..9a1673a 100644 --- a/bemani/format/afp.py +++ b/bemani/format/afp.py @@ -1,5 +1,6 @@ import io import struct +import sys from PIL import Image # type: ignore from typing import Any, Dict, List, Optional, Tuple @@ -98,6 +99,82 @@ class TextureRegion: class SWF: + + END = 0x0 + SHOW_FRAME = 0x1 + DEFINE_SHAPE = 0x2 + PLACE_OBJECT = 0x4 + REMOVE_OBJECT = 0x5 + DEFINE_BITS = 0x6 + DEFINE_BUTTON = 0x7 + JPEG_TABLES = 0x8 + BACKGROUND_COLOR = 0x9 + DEFINE_FONT = 0xa + DEFINE_TEXT = 0xb + DO_ACTION = 0xc + DEFINE_FONT_INFO = 0xd + DEFINE_SOUND = 0xe + START_SOUND = 0xf + DEFINE_BUTTON_SOUND = 0x11 + SOUND_STREAM_HEAD = 0x12 + SOUND_STREAM_BLOCK = 0x13 + DEFINE_BITS_LOSSLESS = 0x14 + DEFINE_BITS_JPEG2 = 0x15 + DEFINE_SHAPE2 = 0x16 + DEFINE_BUTTON_CXFORM = 0x17 + PROTECT = 0x18 + PLACE_OBJECT2 = 0x1a + REMOVE_OBJECT2 = 0x1c + DEFINE_SHAPE3 = 0x20 + DEFINE_TEXT2 = 0x21 + DEFINE_BUTTON2 = 0x22 + DEFINE_BITS_JPEG3 = 0x23 + DEFINE_BITS_LOSSLESS2 = 0x24 + DEFINE_EDIT_TEXT = 0x25 + DEFINE_SPRITE = 0x27 + FRAME_LABEL = 0x2b + SOUND_STREAM_HEAD2 = 0x2d + DEFINE_MORPH_SHAPE = 0x2e + DEFINE_FONT2 = 0x30 + EXPORT_ASSETS = 0x38 + IMPORT_ASSETS = 0x39 + DO_INIT_ACTION = 0x3b + DEFINE_VIDEO_STREAM = 0x3c + VIDEO_FRAME = 0x3d + DEFINE_FONT_INFO2 = 0x3e + ENABLE_DEBUGGER2 = 0x40 + SCRIPT_LIMITS = 0x41 + SET_TAB_INDEX = 0x42 + PLACE_OBJECT3 = 0x46 + IMPORT_ASSETS2 = 0x47 + DEFINE_FONT3 = 0x4b + DEFINE_SCALING_GRID = 0x4e + METADATA = 0x4d + DEFINE_SHAPE4 = 0x53 + DEFINE_MORPH_SHAPE2 = 0x54 + SCENE_LABEL = 0x56 + AFP_IMAGE = 0x64 + AFP_DEFINE_SOUND = 0x65 + AFP_SOUND_STREAM_BLOCK = 0x66 + AFP_DEFINE_FONT = 0x67 + AFP_DEFINE_SHAPE = 0x68 + AEP_PLACE_OBJECT = 0x6e + AP2_DEFINE_FONT = 0x78 + AP2_DEFINE_SPRITE = 0x79 + AP2_DO_ACTION = 0x7a + AP2_DEFINE_BUTTON = 0x7b + AP2_DEFINE_BUTTON_SOUND = 0x7c + AP2_DEFINE_TEXT = 0x7d + AP2_DEFINE_EDIT_TEXT = 0x7e + AP2_PLACE_OBJECT = 0x7f + AP2_REMOVE_OBJECT = 0x80 + AP2_START_SOUND = 0x81 + AP2_DEFINE_MORPH_SHAPE = 0x82 + AP2_IMAGE = 0x83 + AP2_SHAPE = 0x84 + AP2_SOUND = 0x85 + AP2_VIDEO = 0x86 + def __init__( self, name: str, @@ -105,6 +182,7 @@ class SWF: descramble_info: bytes = b"", ) -> None: self.name = name + self.exported_name = "" self.data = data self.descramble_info = descramble_info @@ -112,8 +190,28 @@ class SWF: # sections that we aren't parsing correctly. self.coverage: List[bool] = [False] * len(data) - # Parse the info out. - self.__parse() + def add_coverage(self, offset: int, length: int, unique: bool = True) -> None: + for i in range(offset, offset + length): + if self.coverage[i] and unique: + raise Exception(f"Already covered {hex(offset)}!") + self.coverage[i] = True + + def print_coverage(self) -> None: + # First offset that is not coverd in a run. + start = None + + for offset, covered in enumerate(self.coverage): + if covered: + if start is not None: + print(f"Uncovered: {hex(start)} - {hex(offset)} ({offset-start} bytes)", file=sys.stderr) + start = None + else: + if start is None: + start = offset + if start is not None: + # Print final range + offset = len(self.coverage) + print(f"Uncovered: {hex(start)} - {hex(offset)} ({offset-start} bytes)", file=sys.stderr) def as_dict(self) -> Dict[str, Any]: return { @@ -123,114 +221,165 @@ class SWF: } def tag_to_name(self, tagid: int) -> str: - resources: List[Any] = [ - [['END'], '0x0'], - [['SHOW_FRAME'], '0x1'], - [['DEFINE_SHAPE'], '0x2'], - [['PLACE_OBJECT'], '0x4'], - [['REMOVE_OBJECT'], '0x5'], - [['DEFINE_BITS'], '0x6'], - [['DEFINE_BUTTON'], '0x7'], - [['JPEG_TABLES'], '0x8'], - [['BACKGROUND_COLOR'], '0x9'], - [['DEFINE_FONT'], '0xa'], - [['DEFINE_TEXT'], '0xb'], - [['DO_ACTION'], '0xc'], - [['DEFINE_FONT_INFO'], '0xd'], - [['DEFINE_SOUND'], '0xe'], - [['START_SOUND'], '0xf'], - [['DEFINE_BUTTON_SOUND'], '0x11'], - [['SOUND_STREAM_HEAD'], '0x12'], - [['SOUND_STREAM_BLOCK'], '0x13'], - [['DEFINE_BITS_LOSSLESS'], '0x14'], - [['DEFINE_BITS_JPEG2'], '0x15'], - [['DEFINE_SHAPE2'], '0x16'], - [['DEFINE_BUTTON_CXFORM'], '0x17'], - [['PROTECT'], '0x18'], - [['PLACE_OBJECT2'], '0x1a'], - [['REMOVE_OBJECT2'], '0x1c'], - [['DEFINE_SHAPE3'], '0x20'], - [['DEFINE_TEXT2'], '0x21'], - [['DEFINE_BUTTON2'], '0x22'], - [['DEFINE_BITS_JPEG3'], '0x23'], - [['DEFINE_BITS_LOSSLESS2'], '0x24'], - [['DEFINE_EDIT_TEXT'], '0x25'], - [['DEFINE_SPRITE'], '0x27'], - [['FRAME_LABEL'], '0x2b'], - [['SOUND_STREAM_HEAD2'], '0x2d'], - [['DEFINE_MORPH_SHAPE'], '0x2e'], - [['DEFINE_FONT2'], '0x30'], - [['EXPORT_ASSETS'], '0x38'], - [['IMPORT_ASSETS'], '0x39'], - [['DO_INIT_ACTION'], '0x3b'], - [['DEFINE_VIDEO_STREAM'], '0x3c'], - [['VIDEO_FRAME'], '0x3d'], - [['DEFINE_FONT_INFO2'], '0x3e'], - [['ENABLE_DEBUGGER2'], '0x40'], - [['SCRIPT_LIMITS'], '0x41'], - [['SET_TAB_INDEX'], '0x42'], - [['PLACE_OBJECT3'], '0x46'], - [['IMPORT_ASSETS2'], '0x47'], - [['DEFINE_FONT3'], '0x4b'], - [['DEFINE_SCALING_GRID'], '0x4e'], - [['METADATA'], '0x4d'], - [['DEFINE_SHAPE4'], '0x53'], - [['DEFINE_MORPH_SHAPE2'], '0x54'], - [['SCENE_LABEL?'], '0x56'], - [['AFP_IMAGE'], '0x64'], - [['AFP_DEFINE_SOUND'], '0x65'], - [['AFP_SOUND_STREAM_BLOCK'], '0x66'], - [['AFP_DEFINE_FONT'], '0x67'], - [['AFP_DEFINE_SHAPE'], '0x68'], - [['AEP_PLACE_OBJECT'], '0x6e'], - [['AP2_DEFINE_FONT'], '0x78'], - [['AP2_DEFINE_SPRITE'], '0x79'], - [['AP2_DO_ACTION'], '0x7a'], - [['AP2_DEFINE_BUTTON'], '0x7b'], - [['AP2_DEFINE_BUTTON_SOUND'], '0x7c'], - [['AP2_DEFINE_TEXT'], '0x7d'], - [['AP2_DEFINE_EDIT_TEXT'], '0x7e'], - [['AP2_PLACE_OBJECT'], '0x7f'], - [['AP2_REMOVE_OBJECT'], '0x80'], - [['AP2_START_SOUND'], '0x81'], - [['AP2_DEFINE_MORPH_SHAPE'], '0x82'], - [['AP2_IMAGE'], '0x83'], - [['AP2_SHAPE'], '0x84'], - [['AP2_SOUND'], '0x85'], - [['AP2_VIDEO'], '0x86'], - ] + resources: Dict[int, str] = { + self.END: 'END', + self.SHOW_FRAME: 'SHOW_FRAME', + 0x2: 'DEFINE_SHAPE', + 0x4: 'PLACE_OBJECT', + 0x5: 'REMOVE_OBJECT', + 0x6: 'DEFINE_BITS', + 0x7: 'DEFINE_BUTTON', + 0x8: 'JPEG_TABLES', + 0x9: 'BACKGROUND_COLOR', + 0xa: 'DEFINE_FONT', + 0xb: 'DEFINE_TEXT', + 0xc: 'DO_ACTION', + 0xd: 'DEFINE_FONT_INFO', + 0xe: 'DEFINE_SOUND', + 0xf: 'START_SOUND', + 0x11: 'DEFINE_BUTTON_SOUND', + 0x12: 'SOUND_STREAM_HEAD', + 0x13: 'SOUND_STREAM_BLOCK', + 0x14: 'DEFINE_BITS_LOSSLESS', + 0x15: 'DEFINE_BITS_JPEG2', + 0x16: 'DEFINE_SHAPE2', + 0x17: 'DEFINE_BUTTON_CXFORM', + 0x18: 'PROTECT', + 0x1a: 'PLACE_OBJECT2', + 0x1c: 'REMOVE_OBJECT2', + 0x20: 'DEFINE_SHAPE3', + 0x21: 'DEFINE_TEXT2', + 0x22: 'DEFINE_BUTTON2', + 0x23: 'DEFINE_BITS_JPEG3', + 0x24: 'DEFINE_BITS_LOSSLESS2', + 0x25: 'DEFINE_EDIT_TEXT', + 0x27: 'DEFINE_SPRITE', + 0x2b: 'FRAME_LABEL', + 0x2d: 'SOUND_STREAM_HEAD2', + 0x2e: 'DEFINE_MORPH_SHAPE', + 0x30: 'DEFINE_FONT2', + 0x38: 'EXPORT_ASSETS', + 0x39: 'IMPORT_ASSETS', + 0x3b: 'DO_INIT_ACTION', + 0x3c: 'DEFINE_VIDEO_STREAM', + 0x3d: 'VIDEO_FRAME', + 0x3e: 'DEFINE_FONT_INFO2', + 0x40: 'ENABLE_DEBUGGER2', + 0x41: 'SCRIPT_LIMITS', + 0x42: 'SET_TAB_INDEX', + 0x46: 'PLACE_OBJECT3', + 0x47: 'IMPORT_ASSETS2', + 0x4b: 'DEFINE_FONT3', + 0x4e: 'DEFINE_SCALING_GRID', + 0x4d: 'METADATA', + 0x53: 'DEFINE_SHAPE4', + 0x54: 'DEFINE_MORPH_SHAPE2', + 0x56: 'SCENE_LABEL', + 0x64: 'AFP_IMAGE', + 0x65: 'AFP_DEFINE_SOUND', + 0x66: 'AFP_SOUND_STREAM_BLOCK', + 0x67: 'AFP_DEFINE_FONT', + 0x68: 'AFP_DEFINE_SHAPE', + 0x6e: 'AEP_PLACE_OBJECT', + 0x78: 'AP2_DEFINE_FONT', + 0x79: 'AP2_DEFINE_SPRITE', + 0x7a: 'AP2_DO_ACTION', + 0x7b: 'AP2_DEFINE_BUTTON', + 0x7c: 'AP2_DEFINE_BUTTON_SOUND', + 0x7d: 'AP2_DEFINE_TEXT', + 0x7e: 'AP2_DEFINE_EDIT_TEXT', + 0x7f: 'AP2_PLACE_OBJECT', + 0x80: 'AP2_REMOVE_OBJECT', + 0x81: 'AP2_START_SOUND', + 0x82: 'AP2_DEFINE_MORPH_SHAPE', + 0x83: 'AP2_IMAGE', + self.AP2_SHAPE: 'AP2_SHAPE', + self.AP2_SOUND: 'AP2_SOUND', + self.AP2_VIDEO: 'AP2_VIDEO', + } - for name, tid in resources: - if int(tid, 16) == tagid: - return name[0] - return "UNKNOWN" + return resources.get(tagid, "UNKNOWN") - def __parse_tags(self, ap2_version: int, afp_version: int, ap2data: bytearray, tagdata: bytearray) -> None: - tags_count = struct.unpack(" None: + # Suppress debug text unless asked + if verbose: + def vprint(*args: Any, **kwargs: Any) -> None: # type: ignore + print(*args, **kwargs, file=sys.stderr) - print(f"tags count: {tags_count}") + add_coverage = self.add_coverage + else: + def vprint(*args: Any, **kwargs: Any) -> None: # type: ignore + pass + + def add_coverage(*args: Any, **kwargs: Any) -> None: # type: ignore + pass + + if tagid == self.AP2_SHAPE: + if size != 4: + raise Exception(f"Invalid shape size {size}") + + # TODO, this should be little endian? But it only works out if I do big endian. + _, shape_id = struct.unpack(">HH", self.data[dataoffset:(dataoffset + 4)]) + + add_coverage(dataoffset, size) + + shape_reference = f"{self.exported_name}_shape{shape_id}" + vprint(f" {shape_reference}") + # TODO: Switch on tag types, parse out data. + elif False: + add_coverage(dataoffset, size) + + def __parse_tags(self, ap2_version: int, afp_version: int, ap2data: bytearray, tags_base_offset: int, verbose: bool = False) -> None: + # Suppress debug text unless asked + if verbose: + def vprint(*args: Any, **kwargs: Any) -> None: # type: ignore + print(*args, **kwargs, file=sys.stderr) + + add_coverage = self.add_coverage + else: + def vprint(*args: Any, **kwargs: Any) -> None: # type: ignore + pass + + def add_coverage(*args: Any, **kwargs: Any) -> None: # type: ignore + pass + + tags_unknown1, tags_unknown2, tags_count = struct.unpack("> 22) & 0x3FF size = ((tag & 0x3FFFFF) + 3) & 0xFFFFFFFC # Round to multiple of 4. - print(f"tag: {hex(tagid)} ({self.tag_to_name(tagid)}), size: {size}") + vprint(f" Tag: {hex(tagid)} ({self.tag_to_name(tagid)}), Size: {size}, Offset: {hex(tags_offset + 4)}") + self.__parse_tag(tagid, size, tags_offset + 4, verbose=verbose) + tags_offset += size + 4 # Skip past tag header and data. - imported_tags_count = struct.unpack(" None: + # Suppress debug text unless asked + if verbose: + def vprint(*args: Any, **kwargs: Any) -> None: # type: ignore + print(*args, **kwargs, file=sys.stderr) - print(f"imported tags count: {imported_tags_count}") - for i in range(imported_tags_count): - unknown, size = struct.unpack(" None: # type: ignore + pass + + def add_coverage(*args: Any, **kwargs: Any) -> None: # type: ignore + pass - def __parse(self) -> None: - # TODO: This is incredibly unfinished. swap_len = { 1: 2, 2: 4, @@ -269,9 +418,12 @@ class SWF: offset += 1 return out - print("\n\nstart") + # Start with the basic file header. + magic, length, version, nameoffset, flags, left, right, top, bottom = struct.unpack("<4sIHHIHHHH", data[0:24]) + width = right - left + height = bottom - top + add_coverage(0, 24) - magic, length, version, nameoffset, flags, width, height = struct.unpack("<4sIHHIxxHxxH", data[0:24]) magic = bytes([magic[3] & 0x7F, magic[2] & 0x7F, magic[1] & 0x7F, 0x0]) if magic != b'AP2\x00': raise Exception(f"Unrecognzied magic {magic}!") @@ -279,43 +431,122 @@ class SWF: raise Exception(f"Unexpected length in AFP header, {length} != {len(data)}!") ap2_data_version = magic[0] & 0xFF + if flags & 0x1: + # I have no idea what this is, but its treated as 4 bytes and something + # happens if they aren't all 0xFF. + unknown_bytes = struct.unpack("<4B", data[28:32]) + else: + unknown_bytes = None + add_coverage(28, 4) + if flags & 0x2: # I think this is FPS given the output of this bit of code. fps = struct.unpack(" int: @@ -537,7 +768,9 @@ class AFPFile: def descramble_pman(self, offset: int, verbose: bool) -> PMAN: # Suppress debug text unless asked if verbose: - vprint = print + def vprint(*args: Any, **kwargs: Any) -> None: # type: ignore + print(*args, **kwargs, file=sys.stderr) + add_coverage = self.add_coverage else: def vprint(*args: Any, **kwargs: Any) -> None: # type: ignore @@ -611,7 +844,9 @@ class AFPFile: ) -> None: # Suppress debug text unless asked if verbose: - vprint = print + def vprint(*args: Any, **kwargs: Any) -> None: # type: ignore + print(*args, **kwargs, file=sys.stderr) + add_coverage = self.add_coverage else: def vprint(*args: Any, **kwargs: Any) -> None: # type: ignore @@ -1342,6 +1577,10 @@ class AFPFile: if header_offset != header_length: raise Exception("Failed to parse bitfield of header correctly!") + # Now, parse out the SWF data in each of the SWF structures we found. + for swf in self.swfdata: + swf.parse(verbose) + if verbose: self.print_coverage() diff --git a/bemani/utils/afputils.py b/bemani/utils/afputils.py index 113a107..80e7457 100644 --- a/bemani/utils/afputils.py +++ b/bemani/utils/afputils.py @@ -93,6 +93,12 @@ def main() -> int: metavar="FILE", help="The file to print", ) + print_parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="Display verbuse debugging output", + ) args = parser.parse_args() @@ -302,7 +308,7 @@ def main() -> int: if args.action == "print": # First, parse the file out with open(args.file, "rb") as bfp: - afpfile = AFPFile(bfp.read(), verbose=False) + afpfile = AFPFile(bfp.read(), verbose=args.verbose) # Now, print it print(json.dumps(afpfile.as_dict(), sort_keys=True, indent=4))