1
0
mirror of synced 2025-01-18 22:24:04 +01:00

Allow dumping decompiled bytecode to file for both raw afp/bsi pairs and TXP2 containers.

This commit is contained in:
Jennifer Taylor 2021-05-10 00:12:54 +00:00
parent ee3e272787
commit d9550122de
3 changed files with 167 additions and 20 deletions

View File

@ -1,5 +1,23 @@
from .geo import Shape, DrawParams 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 .container import TXP2File, PMAN, Texture, TextureRegion, Unknown1, Unknown2
from .render import AFPRenderer from .render import AFPRenderer
from .types import Matrix, Color, Point, Rectangle, AP2Tag, AP2Action, AP2Object, AP2Pointer from .types import Matrix, Color, Point, Rectangle, AP2Tag, AP2Action, AP2Object, AP2Pointer
@ -9,6 +27,21 @@ __all__ = [
'Shape', 'Shape',
'DrawParams', 'DrawParams',
'SWF', 'SWF',
'Frame',
'Tag',
'TagPointer',
'AP2ShapeTag',
'AP2DefineFontTag',
'AP2TextChar',
'AP2TextLine',
'AP2DefineMorphShapeTag',
'AP2DefineButtonTag',
'AP2DefineTextTag',
'AP2DoActionTag',
'AP2PlaceObjectTag',
'AP2RemoveObjectTag',
'AP2DefineSpriteTag',
'AP2DefineEditTextTag',
'NamedTagReference', 'NamedTagReference',
'TXP2File', 'TXP2File',
'PMAN', 'PMAN',

View File

@ -177,7 +177,7 @@ class AP2TextLine:
} }
class AP2DefineMorphShape(Tag): class AP2DefineMorphShapeTag(Tag):
def __init__(self, id: int) -> None: def __init__(self, id: int) -> None:
# TODO: I need to figure out what morph shapes actually DO, and take the # TODO: I need to figure out what morph shapes actually DO, and take the
# values that I parsed out store them here... # 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: def __init__(self, id: int) -> None:
# TODO: I need to figure out what buttons actually DO, and take the # TODO: I need to figure out what buttons actually DO, and take the
# values that I parsed out store them here... # values that I parsed out store them here...
@ -326,7 +326,7 @@ class AP2RemoveObjectTag(Tag):
class AP2DefineSpriteTag(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) super().__init__(id)
# The list of tags that this sprite consists of. Sprites are, much like vanilla # 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] = [] self.frames: List[Frame] = []
# Reference LUT for mapping object reference IDs to names a used in bytecode. # 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 # SWF string table. This is used for faster lookup of strings as well as
# tracking which strings in the table have been parsed correctly. # 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) 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 tagid == AP2Tag.AP2_SHAPE:
if size != 4: if size != 4:
raise Exception(f"Invalid shape size {size}") raise Exception(f"Invalid shape size {size}")
@ -961,7 +972,7 @@ class SWF(TrackedCoverage, VerboseOutput):
self.add_coverage(dataoffset + 4, 4) self.add_coverage(dataoffset + 4, 4)
self.vprint(f"{prefix} Tag ID: {sprite_id}") 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) return AP2DefineSpriteTag(sprite_id, tags, frames, references)
elif tagid == AP2Tag.AP2_DEFINE_FONT: elif tagid == AP2Tag.AP2_DEFINE_FONT:
@ -997,7 +1008,7 @@ class SWF(TrackedCoverage, VerboseOutput):
return AP2DefineFontTag(font_id, fontname, xml_prefix, heights, text_indexes) return AP2DefineFontTag(font_id, fontname, xml_prefix, heights, text_indexes)
elif tagid == AP2Tag.AP2_DO_ACTION: elif tagid == AP2Tag.AP2_DO_ACTION:
datachunk = ap2data[dataoffset:(dataoffset + size)] 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) self.add_coverage(dataoffset, size)
return AP2DoActionTag(bytecode) return AP2DoActionTag(bytecode)
@ -1593,7 +1604,7 @@ class SWF(TrackedCoverage, VerboseOutput):
self.vprint(f"{prefix} Floats: {fv1} {fv2} {fv3} {fv4} {fv5} {fv6} {fv7} {fv8}") 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: elif tagid == AP2Tag.AP2_DEFINE_BUTTON:
flags, button_id, source_tags_count, bytecode_count = struct.unpack("<HHHH", ap2data[dataoffset:(dataoffset + 8)]) flags, button_id, source_tags_count, bytecode_count = struct.unpack("<HHHH", ap2data[dataoffset:(dataoffset + 8)])
self.add_coverage(dataoffset, 8) self.add_coverage(dataoffset, 8)
@ -1757,12 +1768,20 @@ class SWF(TrackedCoverage, VerboseOutput):
self.vprint(f"{prefix} Offset: {hex(loc)}, Sound Unk1: {unk1}, Source Tag ID: {sound_source_tag}") self.vprint(f"{prefix} Offset: {hex(loc)}, Sound Unk1: {unk1}, Source Tag ID: {sound_source_tag}")
raise Exception("TODO: Need to examine this section further if I find data with it!") raise Exception("TODO: Need to examine this section further if I find data with it!")
return AP2DefineButton(button_id) return AP2DefineButtonTag(button_id)
else: else:
self.vprint(f"Unknown tag {hex(tagid)} with data {ap2data[dataoffset:(dataoffset + size)]!r}") self.vprint(f"Unknown tag {hex(tagid)} with data {ap2data[dataoffset:(dataoffset + size)]!r}")
raise Exception(f"Unimplemented tag {hex(tagid)}!") raise Exception(f"Unimplemented tag {hex(tagid)}!")
def __parse_tags(self, ap2_version: int, afp_version: int, ap2data: bytes, tags_base_offset: int, sprite_frame: int, prefix: str = "") -> 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( name_reference_flags, name_reference_count, frame_count, tags_count, name_reference_offset, frame_offset, tags_offset = struct.unpack(
"<HHIIIII", "<HHIIIII",
ap2data[tags_base_offset:(tags_base_offset + 24)] ap2data[tags_base_offset:(tags_base_offset + 24)]
@ -1807,19 +1826,19 @@ class SWF(TrackedCoverage, VerboseOutput):
raise Exception(f"Invalid tag size {size} ({hex(size)})") raise Exception(f"Invalid tag size {size} ({hex(size)})")
self.vprint(f"{prefix} Tag: {hex(tagid)} ({AP2Tag.tag_to_name(tagid)}), Size: {hex(size)}, Offset: {hex(tags_offset + 4)}") self.vprint(f"{prefix} Tag: {hex(tagid)} ({AP2Tag.tag_to_name(tagid)}), Size: {hex(size)}, Offset: {hex(tags_offset + 4)}")
tags.append(self.__parse_tag(ap2_version, afp_version, ap2data, tagid, size, tags_offset + 4, tag_to_frame[i] + sprite_frame, prefix=prefix)) tags.append(self.__parse_tag(ap2_version, afp_version, ap2data, tagid, size, tags_offset + 4, sprite, tag_to_frame[i], prefix=prefix))
tags_offset += ((size + 3) & 0xFFFFFFFC) + 4 # Skip past tag header and data, rounding to the nearest 4 bytes. tags_offset += ((size + 3) & 0xFFFFFFFC) + 4 # Skip past tag header and data, rounding to the nearest 4 bytes.
# Finally, parse place object name references. # Finally, parse place object name references.
self.vprint(f"{prefix}Number of Object Name References: {name_reference_count}, Flags: {hex(name_reference_flags)}") self.vprint(f"{prefix}Number of Object Name References: {name_reference_count}, Flags: {hex(name_reference_flags)}")
references: Dict[int, str] = {} references: Dict[str, int] = {}
for i in range(name_reference_count): for i in range(name_reference_count):
index, stringoffset = struct.unpack("<HH", ap2data[name_reference_offset:(name_reference_offset + 4)]) frameno, stringoffset = struct.unpack("<HH", ap2data[name_reference_offset:(name_reference_offset + 4)])
strval = self.__get_string(stringoffset) strval = self.__get_string(stringoffset)
self.add_coverage(name_reference_offset, 4) self.add_coverage(name_reference_offset, 4)
references[index] = strval references[strval] = frameno
self.vprint(f"{prefix} Name Reference: {index}, Name: {strval}") self.vprint(f"{prefix} Frame Number: {frameno}, Name: {strval}")
name_reference_offset += 4 name_reference_offset += 4
return tags, frames, references return tags, frames, references
@ -2014,7 +2033,7 @@ class SWF(TrackedCoverage, VerboseOutput):
# Tag sections # Tag sections
tags_offset = struct.unpack("<I", data[36:40])[0] tags_offset = struct.unpack("<I", data[36:40])[0]
self.add_coverage(36, 4) self.add_coverage(36, 4)
self.tags, self.frames, self.references = self.__parse_tags(ap2_data_version, version, data, tags_offset, 0) self.tags, self.frames, self.references = self.__parse_tags(ap2_data_version, version, data, tags_offset, None)
# Imported tags sections # Imported tags sections
imported_tags_count = struct.unpack("<h", data[34:36])[0] imported_tags_count = struct.unpack("<h", data[34:36])[0]

View File

@ -7,17 +7,67 @@ import os.path
import sys import sys
import textwrap import textwrap
from PIL import Image, ImageDraw # type: ignore from PIL import Image, ImageDraw # type: ignore
from typing import Any, Dict from typing import Any, Dict, List
from bemani.format.afp import TXP2File, Shape, SWF, AFPRenderer, Color from bemani.format.afp import TXP2File, Shape, SWF, Frame, Tag, AP2DoActionTag, AP2PlaceObjectTag, AP2DefineSpriteTag, AFPRenderer, Color
from bemani.format import IFS from bemani.format import IFS
def write_bytecode(swf: SWF, directory: str, verbose: bool=False) -> 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: def main() -> int:
parser = argparse.ArgumentParser(description="Konami AFP graphic file unpacker/repacker") parser = argparse.ArgumentParser(description="Konami AFP graphic file unpacker/repacker")
subparsers = parser.add_subparsers(help='Action to take', dest='action') 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( extract_parser.add_argument(
"file", "file",
metavar="FILE", metavar="FILE",
@ -70,6 +120,12 @@ def main() -> int:
action="store_true", action="store_true",
help="Write binary SWF files to disk", 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 = subparsers.add_parser('update', help='Update relevant textures in a TXP2 container from a directory')
update_parser.add_argument( update_parser.add_argument(
@ -138,6 +194,32 @@ def main() -> int:
help="Display verbuse debugging output", 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 = subparsers.add_parser('parsegeo', help='Parse a raw GEO file previously extracted from an IFS or TXP2 container')
parsegeo_parser.add_argument( parsegeo_parser.add_argument(
"geo", "geo",
@ -390,6 +472,9 @@ def main() -> int:
if not announced.get(texturename, False): if not announced.get(texturename, False):
print(f"Cannot extract sprites from {texturename} because it is not a supported format!") print(f"Cannot extract sprites from {texturename} because it is not a supported format!")
announced[texturename] = True announced[texturename] = True
if args.write_bytecode:
for swf in afpfile.swfdata:
write_bytecode(swf, args.dir, verbose=args.verbose)
if args.action == "update": if args.action == "update":
# First, parse the file out # First, parse the file out
@ -451,6 +536,16 @@ def main() -> int:
swf.parse(verbose=args.verbose) swf.parse(verbose=args.verbose)
print(json.dumps(swf.as_dict(decompile_bytecode=args.decompile_bytecode, verbose=args.verbose), sort_keys=True, indent=4)) 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("<unnamed>", bafp.read(), bbsi.read())
# Now, decompile it
swf.parse(verbose=args.verbose)
write_bytecode(swf, args.directory, verbose=args.verbose)
if args.action == "parsegeo": if args.action == "parsegeo":
# First, load the AFP and BSI files # First, load the AFP and BSI files
with open(args.geo, "rb") as bfp: with open(args.geo, "rb") as bfp: