From 07a1d09cfa51d76e959bdb4ccd392291cfb90022 Mon Sep 17 00:00:00 2001 From: Jennifer Taylor Date: Fri, 15 Jan 2021 01:03:59 +0000 Subject: [PATCH] Initial work to reverse engineering the AFP flash derivative. --- bemani/utils/afputils.py | 362 +++++++++++++++++++++++++++++++++------ 1 file changed, 306 insertions(+), 56 deletions(-) diff --git a/bemani/utils/afputils.py b/bemani/utils/afputils.py index 166f5c4..b93fb06 100644 --- a/bemani/utils/afputils.py +++ b/bemani/utils/afputils.py @@ -104,7 +104,7 @@ class TextureRegion: } -class Animation: +class SWF: def __init__( self, name: str, @@ -122,6 +122,201 @@ class Animation: 'header': "".join(_hex(x) for x in self.header), } + 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'], + ] + + for name, tid in resources: + if int(tid, 16) == tagid: + return name[0] + return "UNKNOWN" + + def parse_tags(self, ap2_version: int, afp_version: int, ap2data: bytearray, tagdata: bytearray) -> None: + 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}") + tags_offset += size + 4 # Skip past tag header and data. + + imported_tags_count = struct.unpack(" None: + # TODO: This is incredibly unfinished, and should probably be moved to bemani.format.afp + swap_len = { + 1: 2, + 2: 4, + 3: 8, + } + + data = bytearray(self.data) + data_offset = 0 + for i in range(0, len(self.header), 2): + swapword = struct.unpack("> 13) & 0x7 + loops = ((swapword >> 7) & 0x3F) + data_offset += offset + + if swap_type == 0: + # Just jump forward based on loops + data_offset += 256 * loops + continue + + if swap_type not in swap_len: + raise Exception(f"Unknown swap type {swap_type}!") + + # Reverse the bytes + for _ in range(loops + 1): + data[data_offset:(data_offset + swap_len[swap_type])] = data[data_offset:(data_offset + swap_len[swap_type])][::-1] + data_offset += swap_len[swap_type] + + def get_until_null(offset: int) -> bytes: + out = b"" + while data[offset] != 0: + out += data[offset:(offset + 1)] + offset += 1 + return out + + print("\n\nstart") + + 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}!") + if length != len(data): + raise Exception(f"Unexpected length in AFP header, {length} != {len(data)}!") + ap2_data_version = magic[0] & 0xFF + + if flags & 0x2: + # I think this is FPS given the output of this bit of code. + fps = struct.unpack("" and magic != b"GE2D": + raise Exception("Invalid magic value in D2EG structure!") + if filesize != len(shape_data): + raise Exception("Unexpected file size for D2EG structure!") + + # Get width/height + endian = "<" if self.endian == ">" else ">" + width, height = struct.unpack(f"{endian}HH", shape_data[20:24]) + + header4, header5 = struct.unpack( + f"{self.endian}II", + shape_data[24:32], + ) + + rect_offset, tex_offset, unk1_offset, label_offset, unk2_offset = struct.unpack( + f"{self.endian}IIIII", + shape_data[32:52], + ) + + label = None + if label_offset != 0: + labelptr = struct.unpack(f"{self.endian}I", shape_data[label_offset:label_offset + 4])[0] + if labelptr is not None: + bytedata = self.get_until_null(shape_offset + labelptr) + label = AFPFile.descramble_text(bytedata, self.text_obfuscated) # NOQA: F841 + + if rect_offset != 0: + floats = struct.unpack( + f"{self.endian}ffffffff", + shape_data[(rect_offset):(rect_offset + 32)] + ) + _rect_offsets = [x for x in floats] # NOQA: F841 + if tex_offset != 0: + floats = struct.unpack( + f"{self.endian}ffffffff", + shape_data[(tex_offset):(tex_offset + 32)] + ) + tex_offsets = [] + for i, flt in enumerate(floats): + tex_offsets.append(flt * (width if ((i & 1) == 0) else height)) + if unk2_offset != 0: + test = struct.unpack( # NOQA: F841 + f"{endian}iii", + shape_data[(unk2_offset):(unk2_offset + 12)] + ) + self.shapes.append( Shape( name, @@ -1048,16 +1298,16 @@ class AFPFile: vprint("Bit 0x010000 - fontinfo; NOT PRESENT") if feature_mask & 0x20000: - # I am beginning to suspect that this is animation/level data. I have + # I am beginning to suspect that this is SWF data/level data. I have # no idea what "afp" is. Games refer to these as "afp streams". offset = struct.unpack(f"{self.endian}I", self.data[header_offset:(header_offset + 4)])[0] add_coverage(header_offset, 4) header_offset += 4 - vprint(f"Bit 0x020000 - animationheaders; offset: {hex(offset)}") + vprint(f"Bit 0x020000 - swfheaders; offset: {hex(offset)}") - if offset > 0 and len(self.animations) > 0: - for i in range(len(self.animations)): + if offset > 0 and len(self.swfdata) > 0: + for i in range(len(self.swfdata)): structure_offset = offset + (i * 12) # First word is always zero, as observed. I am not ENTIRELY sure that @@ -1075,10 +1325,10 @@ class AFPFile: # potentially unsafe as we could rewrite it incorrectly. So, let's assert! raise Exception("Expected a zero in font package header!") - self.animations[i].header = self.data[afp_header:(afp_header + afp_header_length)] + self.swfdata[i].header = self.data[afp_header:(afp_header + afp_header_length)] add_coverage(afp_header, afp_header_length) else: - vprint("Bit 0x020000 - animationheaders; NOT PRESENT") + vprint("Bit 0x020000 - swfheaders; NOT PRESENT") if feature_mask & 0x40000: vprint("Bit 0x040000 - modern lz mode on") @@ -1414,38 +1664,38 @@ class AFPFile: body += entry2.data if self.features & 0x800: - # This is the names and locations of the animations as far as I can tell. + # This is the names and locations of the SWF data as far as I can tell. offset = AFPFile.align(len(body)) body = AFPFile.pad(body, offset) - bitchunks[11] = struct.pack(f"{self.endian}II", len(self.animations), offset) + bitchunks[11] = struct.pack(f"{self.endian}II", len(self.swfdata), offset) - # Now, calculate where we can put animations and their names. - animation_offset = AFPFile.align(len(body) + (len(self.animations) * 12)) - string_offset = AFPFile.align(animation_offset + sum(AFPFile.align(len(a.data)) for a in self.animations)) - animdata = b"" + # Now, calculate where we can put SWF data and their names. + swfdata_offset = AFPFile.align(len(body) + (len(self.swfdata) * 12)) + string_offset = AFPFile.align(swfdata_offset + sum(AFPFile.align(len(a.data)) for a in self.swfdata)) + swfdata = b"" # Now, lay them out. - for animation in self.animations: - if animation.name not in string_offsets: + for data in self.swfdata: + if data.name not in string_offsets: # We haven't written this string out yet, so put it on our pending list. - pending_strings[animation.name] = string_offset - string_offsets[animation.name] = string_offset + pending_strings[data.name] = string_offset + string_offsets[data.name] = string_offset # Room for the null byte! - string_offset += len(animation.name) + 1 + string_offset += len(data.name) + 1 # Write out the chunk itself. body += struct.pack( f"{self.endian}III", - string_offsets[animation.name], - len(animation.data), - animation_offset + len(animdata), + string_offsets[data.name], + len(data.data), + swfdata_offset + len(swfdata), ) - animdata += AFPFile.pad(animation.data, AFPFile.align(len(animation.data))) + swfdata += AFPFile.pad(data.data, AFPFile.align(len(data.data))) # Now, lay out the data itself and finally string names. - body = self.write_strings(body + animdata, pending_strings) + body = self.write_strings(body + swfdata, pending_strings) pending_strings = {} if self.features & 0x2000: @@ -1522,13 +1772,13 @@ class AFPFile: body = self.write_pman(body, offset, self.unk_pman2, string_offsets) if self.features & 0x1000: - # Mapping of animations to their ID. + # Mapping of SWF data to their ID. offset = AFPFile.align(len(body)) body = AFPFile.pad(body, offset) # Lay down PMAN pointer and PMAN structure itself. bitchunks[12] = struct.pack(f"{self.endian}I", offset) - body = self.write_pman(body, offset, self.animmap, string_offsets) + body = self.write_pman(body, offset, self.swfmap, string_offsets) if self.features & 0x4000: # Mapping of shapes to their ID. @@ -1571,29 +1821,29 @@ class AFPFile: raise Exception("This should not be possible!") if self.features & 0x20000: - # Animation header information. + # SWF header information. offset = AFPFile.align(len(body)) body = AFPFile.pad(body, offset) bitchunks[17] = struct.pack(f"{self.endian}I", offset) - # Now, calculate where we can put animation headers. - animation_offset = AFPFile.align(len(body) + (len(self.animations) * 12)) - animheader = b"" + # Now, calculate where we can put SWF headers. + swfdata_offset = AFPFile.align(len(body) + (len(self.swfdata) * 12)) + swfheader = b"" # Now, lay them out. - for animation in self.animations: + for data in self.swfdata: # Write out the chunk itself. body += struct.pack( f"{self.endian}III", 0, - len(animation.header), - animation_offset + len(animheader), + len(data.header), + swfdata_offset + len(swfheader), ) - animheader += AFPFile.pad(animation.header, AFPFile.align(len(animation.header))) + swfheader += AFPFile.pad(data.header, AFPFile.align(len(data.header))) # Now, lay out the header itself - body += animheader + body += swfheader if self.features & 0x01: # Now, go back and add texture data to the end of the file, fixing up the