From 79b31c7fa2c1a44b40df64de3ef68d71b02ba15c Mon Sep 17 00:00:00 2001 From: Jennifer Taylor Date: Mon, 12 Apr 2021 03:10:31 +0000 Subject: [PATCH] Hook up parsed objects to all of SWF except for the bytecode mess. --- bemani/format/afp/swf.py | 269 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 250 insertions(+), 19 deletions(-) diff --git a/bemani/format/afp/swf.py b/bemani/format/afp/swf.py index facd928..3ea204b 100644 --- a/bemani/format/afp/swf.py +++ b/bemani/format/afp/swf.py @@ -24,6 +24,171 @@ class NamedTagReference: return f"{self.swf}.{self.tag}" +class ByteCode: + # A list of bytecodes to execute. + def __init__(self) -> None: + # TODO: I need to actually come up with some internal representation of this. As far + # as I can tell, games execute bytecode by switch statement dirctly loading the opcodes + # from memory. There is no dynamic recompilation happening and they don't parse anything. + pass + + +class TagPointer: + # A pointer to a tag in this SWF by Tag ID and containing an optional initialization bytecode + # to run for this tag when it is placed/executed. + def __init__(self, id: Optional[int], init_bytecode: Optional[ByteCode] = None) -> None: + self.id = id + self.init_bytecode = init_bytecode + + +class Frame: + def __init__(self, start_tag_offset: int, num_tags: int, imported_tags: List[TagPointer] = []) -> None: + # The start tag offset into the tag list where we should begin placing/executing tags for this frame. + self.start_tag_offset = start_tag_offset + + # The number of tags to pace/execute during this frame. + self.num_tags = num_tags + + # A list of any imported tags that are to be placed this frame. + self.imported_tags = imported_tags + + +class Tag: + # Any tag that can appear in the SWF. All tags will subclass from this for their behavior. + def __init__(self, id: Optional[int]) -> None: + self.id = id + + +class AP2ShapeTag(Tag): + def __init__(self, id: int, reference: str) -> None: + super().__init__(id) + + # The reference is the name of a shape (geo structure) that defines this primitive or sprite. + self.reference = reference + + +class AP2DefineFontTag(Tag): + def __init__(self, id: int, fontname: str, xml_prefix: str, heights: List[int]) -> None: + super().__init__(id) + + # The font name is just the pretty name of the font. + self.fontname = fontname + + # The XML prefix is the reference into any font XML to look up individual + # glyphs for a font in a texture map. + self.xml_prefix = xml_prefix + + # The list of heights are concatenated with the above XML prefix and the + # unicode glyph you want to display, to find the corresponding location + # in the texture map. + self.heights = heights + + +class AP2DoActionTag(Tag): + def __init__(self, bytecode: ByteCode) -> None: + # Do Action Tags are not identified by any tag ID. + super().__init__(None) + + # The bytecode is the actual execution that we expect to perform once + # this tag is placed/executed. + self.bytecode = bytecode + + +class AP2PlaceObjectTag(Tag): + def __init__( + self, + object_id: int, + depth: int, + src_tag_id: Optional[int], + name: Optional[str], + blend: Optional[int], + update: bool, + transform: Optional[Matrix], + color: Optional[Color], + alpha_color: Optional[Color], + triggers: Dict[int, List[ByteCode]], + ) -> None: + # Place Object Tags are not identified by any tag ID. + super().__init__(None) + + # The object ID that we should associate with this object, for removal + # and presumably update and other uses. Not the same as Tag ID. + self.object_id = object_id + + # The depth (level) that we should remove objects from. + self.depth = depth + + # The source tag ID (should point at an AP2ShapeTag by ID) if present. + self.source_tag_id = src_tag_id + + # The name of this object, if present. + self.name = name + + # The blend mode of this object, if present. + self.blend = blend + + # Whether this is an object update (True) or a new object (False). + self.update = update + + # Whether there is a transform matrix to apply before placing/updating this object or not. + self.transform = transform + + # If there is a color to blend with the sprite/shape when drawing. + self.color = color + + # If there is an alpha color to draw instead of draing over any other placed object. + self.alpha_color = alpha_color + + # List of triggers for this object, and their respective bytecodes to execute when the trigger + # fires. + self.triggers = triggers + + +class AP2RemoveObjectTag(Tag): + def __init__(self, object_id: int, depth: int) -> None: + # Remove Object Tags are not identified by any tag ID. + super().__init__(None) + + # The object ID that we should remove, or 0 if we should only remove by depth. + self.object_id = object_id + + # The depth (level) that we should remove objects from. + self.depth = depth + + +class AP2DefineSpriteTag(Tag): + def __init__(self, id: int, tags: List[Tag], frames: List[Frame]) -> None: + super().__init__(id) + + # The list of tags that this sprite consists of. Sprites are, much like vanilla + # SWFs, basically entire SWF movies embedded in them. + self.tags = tags + + # The list of frames this SWF occupies. + self.frames = frames + + +class AP2DefineEditTextTag(Tag): + def __init__(self, id: int, font_tag_id: int, font_height: int, rect: Rectangle, color: Color, default_text: Optional[str] = None) -> None: + super().__init__(id) + + # The ID of the Ap2DefineFontTag that we want to use for the text. + self.font_tag_id = font_tag_id + + # The height we want to select for the text (must be one of the heights in + # the referenced Ap2DefineFontTag tag). + self.font_height = font_height + + # The bounding rectangle for this exit text control. + self.rect = rect + + # The text color we want to use when displaying the text. + self.color = color + + # The default text that should be present in the control when it is initially placed/executed. + self.default_text = default_text + + class SWF(TrackedCoverage, VerboseOutput): def __init__( self, @@ -65,6 +230,13 @@ class SWF(TrackedCoverage, VerboseOutput): # Imported tags, indexed by their Tag ID, and pointing at the SWF asset and exported tag name. self.imported_tags: Dict[int, NamedTagReference] = {} + # Actual tags for this SWF, ordered by their appearance in the file. + self.tags: List[Tag] = [] + + # Frames of this SWF, with the tag offset in the above list and number of tags to + # "execute" that frame. + self.frames: List[Frame] = [] + # 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]] = {} @@ -87,7 +259,7 @@ class SWF(TrackedCoverage, VerboseOutput): 'descramble_info': "".join(_hex(x) for x in self.descramble_info), } - def __parse_bytecode(self, datachunk: bytes, string_offsets: List[int] = [], prefix: str = "") -> None: + def __parse_bytecode(self, datachunk: bytes, string_offsets: List[int] = [], prefix: str = "") -> ByteCode: # First, we need to check if this is a SWF-style bytecode or an AP2 bytecode. ap2_sentinel = struct.unpack(" None: + # TODO: I need to actually parse this, its a mess... + return ByteCode() + + def __parse_tag(self, ap2_version: int, afp_version: int, ap2data: bytes, tagid: int, size: int, dataoffset: int, prefix: str = "") -> Tag: if tagid == AP2Tag.AP2_SHAPE: if size != 4: raise Exception(f"Invalid shape size {size}") - _, shape_id = struct.unpack(" None: + def __parse_tags(self, ap2_version: int, afp_version: int, ap2data: bytes, tags_base_offset: int, prefix: str = "") -> Tuple[List[Tag], List[Frame]]: unknown_tags_flags, unknown_tags_count, frame_count, tags_count, unknown_tags_offset, frame_offset, tags_offset = struct.unpack( "> 20) & 0xFFF + frames.append(Frame(start_tag_offset, num_tags_to_play)) - self.vprint(f"{prefix} Frame Start Tag: {start_tag_id}, Count: {num_tags_to_play}") + self.vprint(f"{prefix} Frame Start Tag: {start_tag_offset}, 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 @@ -806,6 +1029,8 @@ class SWF(TrackedCoverage, VerboseOutput): self.vprint(f"{prefix} Unknown Tag: {hex(unk1)} Name: {strval}") unknown_tags_offset += 4 + return tags, frames + def __descramble(self, scrambled_data: bytes, descramble_info: bytes) -> bytes: swap_len = { 1: 2, @@ -996,7 +1221,7 @@ class SWF(TrackedCoverage, VerboseOutput): # Tag sections tags_offset = struct.unpack("= len(self.frames): + raise Exception(f"Unexpected frame {frame}, we only have {len(self.frames)} frames in this movie!") + self.frames[frame].imported_tags.append(TagPointer(tag_id, bytecode)) if verbose: self.print_coverage()