From d9550122de637c43ac95bcaa84e466bc254e08b2 Mon Sep 17 00:00:00 2001 From: Jennifer Taylor Date: Mon, 10 May 2021 00:12:54 +0000 Subject: [PATCH] Allow dumping decompiled bytecode to file for both raw afp/bsi pairs and TXP2 containers. --- bemani/format/afp/__init__.py | 35 +++++++++++- bemani/format/afp/swf.py | 51 +++++++++++------ bemani/utils/afputils.py | 101 +++++++++++++++++++++++++++++++++- 3 files changed, 167 insertions(+), 20 deletions(-) diff --git a/bemani/format/afp/__init__.py b/bemani/format/afp/__init__.py index 9135f95..43b21f0 100644 --- a/bemani/format/afp/__init__.py +++ b/bemani/format/afp/__init__.py @@ -1,5 +1,23 @@ from .geo import Shape, DrawParams -from .swf import SWF, NamedTagReference +from .swf import ( + SWF, + NamedTagReference, + Frame, + Tag, + TagPointer, + AP2ShapeTag, + AP2DefineFontTag, + AP2TextChar, + AP2TextLine, + AP2DefineMorphShapeTag, + AP2DefineButtonTag, + AP2DefineTextTag, + AP2DoActionTag, + AP2PlaceObjectTag, + AP2RemoveObjectTag, + AP2DefineSpriteTag, + AP2DefineEditTextTag, +) from .container import TXP2File, PMAN, Texture, TextureRegion, Unknown1, Unknown2 from .render import AFPRenderer from .types import Matrix, Color, Point, Rectangle, AP2Tag, AP2Action, AP2Object, AP2Pointer @@ -9,6 +27,21 @@ __all__ = [ 'Shape', 'DrawParams', 'SWF', + 'Frame', + 'Tag', + 'TagPointer', + 'AP2ShapeTag', + 'AP2DefineFontTag', + 'AP2TextChar', + 'AP2TextLine', + 'AP2DefineMorphShapeTag', + 'AP2DefineButtonTag', + 'AP2DefineTextTag', + 'AP2DoActionTag', + 'AP2PlaceObjectTag', + 'AP2RemoveObjectTag', + 'AP2DefineSpriteTag', + 'AP2DefineEditTextTag', 'NamedTagReference', 'TXP2File', 'PMAN', diff --git a/bemani/format/afp/swf.py b/bemani/format/afp/swf.py index 1a18e0b..524ea71 100644 --- a/bemani/format/afp/swf.py +++ b/bemani/format/afp/swf.py @@ -177,7 +177,7 @@ class AP2TextLine: } -class AP2DefineMorphShape(Tag): +class AP2DefineMorphShapeTag(Tag): def __init__(self, id: int) -> None: # TODO: I need to figure out what morph shapes actually DO, and take the # values that I parsed out store them here... @@ -189,7 +189,7 @@ class AP2DefineMorphShape(Tag): } -class AP2DefineButton(Tag): +class AP2DefineButtonTag(Tag): def __init__(self, id: int) -> None: # TODO: I need to figure out what buttons actually DO, and take the # values that I parsed out store them here... @@ -326,7 +326,7 @@ class AP2RemoveObjectTag(Tag): class AP2DefineSpriteTag(Tag): - def __init__(self, id: int, tags: List[Tag], frames: List[Frame], references: Dict[int, str]) -> None: + def __init__(self, id: int, tags: List[Tag], frames: List[Frame], references: Dict[str, int]) -> None: super().__init__(id) # The list of tags that this sprite consists of. Sprites are, much like vanilla @@ -428,7 +428,7 @@ class SWF(TrackedCoverage, VerboseOutput): self.frames: List[Frame] = [] # Reference LUT for mapping object reference IDs to names a used in bytecode. - self.references: Dict[int, str] = {} + self.references: Dict[str, int] = {} # SWF string table. This is used for faster lookup of strings as well as # tracking which strings in the table have been parsed correctly. @@ -934,7 +934,18 @@ class SWF(TrackedCoverage, VerboseOutput): return ByteCode(bytecode_name, actions, offset_ptr) - def __parse_tag(self, ap2_version: int, afp_version: int, ap2data: bytes, tagid: int, size: int, dataoffset: int, tag_frame: int, prefix: str = "") -> Tag: + def __parse_tag( + self, + ap2_version: int, + afp_version: int, + ap2data: bytes, + tagid: int, + size: int, + dataoffset: int, + tag_parent_sprite: Optional[int], + tag_frame: int, + prefix: str = "", + ) -> Tag: if tagid == AP2Tag.AP2_SHAPE: if size != 4: raise Exception(f"Invalid shape size {size}") @@ -961,7 +972,7 @@ class SWF(TrackedCoverage, VerboseOutput): self.add_coverage(dataoffset + 4, 4) self.vprint(f"{prefix} Tag ID: {sprite_id}") - tags, frames, references = self.__parse_tags(ap2_version, afp_version, ap2data, subtags_offset, tag_frame, prefix=" " + prefix) + tags, frames, references = self.__parse_tags(ap2_version, afp_version, ap2data, subtags_offset, sprite_id, prefix=" " + prefix) return AP2DefineSpriteTag(sprite_id, tags, frames, references) elif tagid == AP2Tag.AP2_DEFINE_FONT: @@ -997,7 +1008,7 @@ class SWF(TrackedCoverage, VerboseOutput): return AP2DefineFontTag(font_id, fontname, xml_prefix, heights, text_indexes) elif tagid == AP2Tag.AP2_DO_ACTION: datachunk = ap2data[dataoffset:(dataoffset + size)] - bytecode = self.__parse_bytecode(f"on_enter_frame_{tag_frame}", datachunk, prefix=prefix) + bytecode = self.__parse_bytecode(f"on_enter_{f'sprite_{tag_parent_sprite}' if tag_parent_sprite is not None else 'main'}_frame_{tag_frame}", datachunk, prefix=prefix) self.add_coverage(dataoffset, size) return AP2DoActionTag(bytecode) @@ -1593,7 +1604,7 @@ class SWF(TrackedCoverage, VerboseOutput): self.vprint(f"{prefix} Floats: {fv1} {fv2} {fv3} {fv4} {fv5} {fv6} {fv7} {fv8}") - return AP2DefineMorphShape(define_shape_id) + return AP2DefineMorphShapeTag(define_shape_id) elif tagid == AP2Tag.AP2_DEFINE_BUTTON: flags, button_id, source_tags_count, bytecode_count = struct.unpack(" Tuple[List[Tag], List[Frame], Dict[int, str]]: + def __parse_tags( + self, + ap2_version: int, + afp_version: int, + ap2data: bytes, + tags_base_offset: int, + sprite: Optional[int], + prefix: str = "", + ) -> Tuple[List[Tag], List[Frame], Dict[str, int]]: name_reference_flags, name_reference_count, frame_count, tags_count, name_reference_offset, frame_offset, tags_offset = struct.unpack( " None: + # Actually place the files down. + os.makedirs(directory, exist_ok=True) + + # Buffer for where the decompiled data goes. + buff: List[str] = [] + lut: Dict[str, int] = {} + + def bytecode_from_frames(frames: List[Frame]) -> None: + for frame in frames: + for tag in frame.imported_tags: + if tag.init_bytecode: + buff.append(tag.init_bytecode.decompile(verbose=verbose)) + + def bytecode_from_tags(tags: List[Tag]) -> None: + for tag in tags: + if isinstance(tag, AP2DoActionTag): + buff.append(tag.bytecode.decompile(verbose=verbose)) + elif isinstance(tag, AP2PlaceObjectTag): + for _, triggers in tag.triggers.items(): + for trigger in triggers: + buff.append(trigger.decompile(verbose=verbose)) + elif isinstance(tag, AP2DefineSpriteTag): + lut.update(tag.references) + bytecode_from_frames(tag.frames) + bytecode_from_tags(tag.tags) + + lut.update(swf.references) + bytecode_from_frames(swf.frames) + bytecode_from_tags(swf.tags) + + # If we have references, put them at the top as global defines. + if lut: + buff = [ + os.linesep.join([ + '// Defined string references from SWF container, as used for frame lookups.', + 'FRAME_LUT = {', + *[f" {name!r}: {frame}," for name, frame in lut.items()], + '};', + ]), + *buff, + ] + + # Now, write it out. + filename = os.path.join(directory, swf.exported_name) + ".code" + print(f"Writing code to {filename}...") + with open(filename, "wb") as bfp: + bfp.write(f"{os.linesep}{os.linesep}".join(buff).encode('utf-8')) + + def main() -> int: parser = argparse.ArgumentParser(description="Konami AFP graphic file unpacker/repacker") subparsers = parser.add_subparsers(help='Action to take', dest='action') - extract_parser = subparsers.add_parser('extract', help='Extract relevant textures from TXP2 container') + extract_parser = subparsers.add_parser('extract', help='Extract relevant file data and textures from a TXP2 container') extract_parser.add_argument( "file", metavar="FILE", @@ -70,6 +120,12 @@ def main() -> int: action="store_true", help="Write binary SWF files to disk", ) + extract_parser.add_argument( + "-y", + "--write-bytecode", + action="store_true", + help="Write decompiled bytecode files to disk", + ) update_parser = subparsers.add_parser('update', help='Update relevant textures in a TXP2 container from a directory') update_parser.add_argument( @@ -138,6 +194,32 @@ def main() -> int: help="Display verbuse debugging output", ) + decompile_parser = subparsers.add_parser('decompile', help='Decompile bytecode in a raw AFP/BSI file pair previously extracted from an IFS or TXP2 container') + decompile_parser.add_argument( + "afp", + metavar="AFPFILE", + help="The AFP file to parse", + ) + decompile_parser.add_argument( + "bsi", + metavar="BSIFILE", + help="The BSI file to parse", + ) + decompile_parser.add_argument( + "-d", + "--directory", + metavar="DIR", + default='.', + type=str, + help="Directory to extract to after decompiling. Defaults to current directory.", + ) + decompile_parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="Display verbuse debugging output", + ) + parsegeo_parser = subparsers.add_parser('parsegeo', help='Parse a raw GEO file previously extracted from an IFS or TXP2 container') parsegeo_parser.add_argument( "geo", @@ -390,6 +472,9 @@ def main() -> int: if not announced.get(texturename, False): print(f"Cannot extract sprites from {texturename} because it is not a supported format!") announced[texturename] = True + if args.write_bytecode: + for swf in afpfile.swfdata: + write_bytecode(swf, args.dir, verbose=args.verbose) if args.action == "update": # First, parse the file out @@ -451,6 +536,16 @@ def main() -> int: swf.parse(verbose=args.verbose) print(json.dumps(swf.as_dict(decompile_bytecode=args.decompile_bytecode, verbose=args.verbose), sort_keys=True, indent=4)) + if args.action == "decompile": + # First, load the AFP and BSI files + with open(args.afp, "rb") as bafp: + with open(args.bsi, "rb") as bbsi: + swf = SWF("", bafp.read(), bbsi.read()) + + # Now, decompile it + swf.parse(verbose=args.verbose) + write_bytecode(swf, args.directory, verbose=args.verbose) + if args.action == "parsegeo": # First, load the AFP and BSI files with open(args.geo, "rb") as bfp: