From 9f6b9eb7d6b41918934ca4f20c417a1b10d18e64 Mon Sep 17 00:00:00 2001 From: Jennifer Taylor Date: Mon, 12 Apr 2021 03:09:57 +0000 Subject: [PATCH] Begin parsing AFP data into useful structures. --- bemani/format/afp/__init__.py | 3 +- bemani/format/afp/swf.py | 120 +++++++++++++++++++++-------- bemani/format/afp/types/generic.py | 12 +++ 3 files changed, 102 insertions(+), 33 deletions(-) diff --git a/bemani/format/afp/__init__.py b/bemani/format/afp/__init__.py index 6c929e3..9ea22f5 100644 --- a/bemani/format/afp/__init__.py +++ b/bemani/format/afp/__init__.py @@ -1,5 +1,5 @@ from .geo import Shape, DrawParams -from .swf import SWF +from .swf import SWF, NamedTagReference from .container import TXP2File, PMAN, Texture, TextureRegion, Unknown1, Unknown2 from .types import Matrix, Color, Point, Rectangle, AP2Tag, AP2Action, AP2Object, AP2Pointer, AP2Property @@ -8,6 +8,7 @@ __all__ = [ 'Shape', 'DrawParams', 'SWF', + 'NamedTagReference', 'TXP2File', 'PMAN', 'Texture', diff --git a/bemani/format/afp/swf.py b/bemani/format/afp/swf.py index 981a2be..facd928 100644 --- a/bemani/format/afp/swf.py +++ b/bemani/format/afp/swf.py @@ -2,13 +2,28 @@ from hashlib import md5 import os import struct import sys -from typing import Any, Dict, List, Tuple +from typing import Any, Dict, List, Optional, Tuple from .types import Matrix, Color, Point, Rectangle from .types import AP2Action, AP2Tag, AP2Property from .util import TrackedCoverage, VerboseOutput, _hex +class NamedTagReference: + def __init__(self, swf_name: str, tag_name: str) -> None: + self.swf = swf_name + self.tag = tag_name + + def as_dict(self) -> Dict[str, Any]: + return { + 'swf': self.swf, + 'tag': self.tag, + } + + def __repr__(self) -> str: + return f"{self.swf}.{self.tag}" + + class SWF(TrackedCoverage, VerboseOutput): def __init__( self, @@ -19,22 +34,47 @@ class SWF(TrackedCoverage, VerboseOutput): # First, init the coverage engine. super().__init__() - # Now, initialize parsed data. - self.name = name - self.exported_name = "" - self.data = data - self.descramble_info = descramble_info + # Name of this SWF, according to the container it was extracted from. + self.name: str = name - # Initialize string table. This is used for faster lookup of strings - # as well as tracking which strings in the table have been parsed correctly. - self.strings: Dict[int, Tuple[str, bool]] = {} + # Name of this SWF, as referenced by other SWFs that require imports from it. + self.exported_name: str = "" + + # Full, unparsed data for this SWF, as well as the descrambling headers. + self.data: bytes = data + self.descramble_info: bytes = descramble_info + + # Data version of this SWF. + self.data_version: int = 0 + + # Container version of this SWF. + self.container_version: int = 0 + + # The requested frames per second this SWF plays at. + self.fps: float = 0.0 + + # Background color of this SWF movie. + self.color: Optional[Color] = None + + # Location of this SWF in screen space. + self.location: Rectangle = Rectangle.Empty() + + # Exported tags, indexed by their name and pointing to the Tag ID that name identifies. + self.exported_tags: Dict[str, int] = {} + + # Imported tags, indexed by their Tag ID, and pointing at the SWF asset and exported tag name. + self.imported_tags: Dict[int, NamedTagReference] = {} + + # SWF string table. This is used for faster lookup of strings as well as + # tracking which strings in the table have been parsed correctly. + self.__strings: Dict[int, Tuple[str, bool]] = {} def print_coverage(self) -> None: # First print uncovered bytes super().print_coverage() # Now, print uncovered strings - for offset, (string, covered) in self.strings.items(): + for offset, (string, covered) in self.__strings.items(): if covered: continue @@ -752,7 +792,7 @@ class SWF(TrackedCoverage, VerboseOutput): start_tag_id = frame_info & 0xFFFFF num_tags_to_play = (frame_info >> 20) & 0xFFF - self.vprint(f"{prefix} Frame Start Tag: {hex(start_tag_id)}, Count: {num_tags_to_play}") + self.vprint(f"{prefix} Frame Start Tag: {start_tag_id}, Count: {num_tags_to_play}") frame_offset += 4 # Now, parse unknown tags? I have no idea what these are, but they're referencing strings that @@ -814,7 +854,7 @@ class SWF(TrackedCoverage, VerboseOutput): if byte == 0: if curstring: # We found a string! - self.strings[curloc - stringtable_offset] = (bytes(curstring).decode('utf8'), False) + self.__strings[curloc - stringtable_offset] = (bytes(curstring).decode('utf8'), False) curloc = stringtable_offset + i + 1 curstring = [] curloc = stringtable_offset + i + 1 @@ -824,7 +864,7 @@ class SWF(TrackedCoverage, VerboseOutput): if curstring: raise Exception("Logic error!") - if 0 in self.strings: + if 0 in self.__strings: raise Exception("Should not include null string!") return bytes(data) @@ -833,8 +873,8 @@ class SWF(TrackedCoverage, VerboseOutput): if offset == 0: return "" - self.strings[offset] = (self.strings[offset][0], True) - return self.strings[offset][0] + self.__strings[offset] = (self.__strings[offset][0], True) + return self.__strings[offset][0] def parse(self, verbose: bool = False) -> None: with self.covered(len(self.data), verbose): @@ -847,8 +887,6 @@ class SWF(TrackedCoverage, VerboseOutput): # 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 self.add_coverage(0, 24) ap2_data_version = magic[0] & 0xFF @@ -862,24 +900,40 @@ class SWF(TrackedCoverage, VerboseOutput): if version != 0x200: raise Exception(f"Unsupported AP2 version {version}!") + # The container version is analogous to the SWF file version. I'm pretty sure it + # dictates certain things like what properties are available. These appear strictly + # additive so we don't concern ourselves with this. + self.container_version = ap2_data_version + + # The data version is basically used for how to parse tags. There was an older data + # version 0x100 that used more SWF-like bit-packed tags and while lots of code exists + # to parse this, the AP2 libraries will reject SWF data with this version. + self.data_version = version + + # As far as I can tell, most things only care about the width and height of this + # movie, and I think the Shapes are rendered based on the width/height. However, it + # can have a non-zero x/y offset and I think this is used when rendering multiple + # movie clips? + self.location = Rectangle(left=left, right=right, top=top, bottom=bottom) + if flags & 0x1: # This appears to be the animation background color. rgba = struct.unpack("> 8) & 0xFF) / 255.0, b=((rgba >> 16) & 0xFF) / 255.0, a=((rgba >> 24) & 0xFF) / 255.0, ) else: - swf_color = None + self.color = None self.add_coverage(28, 4) if flags & 0x2: # FPS can be either an integer or a float. - fps = struct.unpack(" float: + return self.right - self.left + + @property + def height(self) -> float: + return self.bottom - self.top + def __repr__(self) -> str: return f"left: {round(self.left, 5)}, top: {round(self.top, 5)}, bottom: {round(self.bottom, 5)}, right: {round(self.right, 5)}" + + @staticmethod + def Empty() -> "Rectangle": + return Rectangle(left=0.0, right=0.0, top=0.0, bottom=0.0)