2020-11-06 03:08:21 +01:00
#! /usr/bin/env python3
import argparse
2021-04-16 01:18:33 +02:00
import io
2020-11-27 18:47:48 +01:00
import json
2021-05-21 23:47:26 +02:00
import math
2020-11-06 03:08:21 +01:00
import os
import os . path
import sys
2020-11-06 17:18:09 +01:00
import textwrap
2020-11-23 21:56:05 +01:00
from PIL import Image , ImageDraw # type: ignore
2021-05-10 02:12:54 +02:00
from typing import Any , Dict , List
2020-11-06 03:08:21 +01:00
2021-05-21 23:31:13 +02:00
from bemani . format . afp import TXP2File , Shape , SWF , Frame , Tag , AP2DoActionTag , AP2PlaceObjectTag , AP2DefineSpriteTag , AFPRenderer , Color , Matrix
2021-04-16 01:18:33 +02:00
from bemani . format import IFS
2020-11-30 19:16:02 +01:00
2020-11-06 03:08:21 +01:00
2021-05-10 02:12:54 +02:00
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 ) :
2021-05-11 00:26:46 +02:00
lut . update ( tag . labels )
2021-05-10 02:12:54 +02:00
bytecode_from_frames ( tag . frames )
bytecode_from_tags ( tag . tags )
2021-05-11 00:26:46 +02:00
lut . update ( swf . labels )
2021-05-10 02:12:54 +02:00
bytecode_from_frames ( swf . frames )
bytecode_from_tags ( swf . tags )
2021-05-11 00:26:46 +02:00
# If we have frame labels, put them at the top as global defines.
2021-05-10 02:12:54 +02:00
if lut :
buff = [
os . linesep . join ( [
2021-05-11 00:26:46 +02:00
' // Defined frame labels from SWF container, as used for frame lookups. ' ,
2021-05-10 02:12:54 +02:00
' 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 ' ) )
2020-11-06 03:08:21 +01:00
def main ( ) - > int :
2020-11-11 04:39:51 +01:00
parser = argparse . ArgumentParser ( description = " Konami AFP graphic file unpacker/repacker " )
subparsers = parser . add_subparsers ( help = ' Action to take ' , dest = ' action ' )
2021-05-10 02:12:54 +02:00
extract_parser = subparsers . add_parser ( ' extract ' , help = ' Extract relevant file data and textures from a TXP2 container ' )
2020-11-11 04:39:51 +01:00
extract_parser . add_argument (
2020-11-06 03:08:21 +01:00
" file " ,
metavar = " FILE " ,
help = " The file to extract " ,
)
2020-11-11 04:39:51 +01:00
extract_parser . add_argument (
2020-11-06 03:08:21 +01:00
" dir " ,
metavar = " DIR " ,
help = " Directory to extract to " ,
)
2020-11-11 04:39:51 +01:00
extract_parser . add_argument (
2020-11-06 03:08:21 +01:00
" -p " ,
" --pretend " ,
action = " store_true " ,
2020-11-11 04:39:51 +01:00
help = " Pretend to extract instead of extracting " ,
2020-11-06 03:08:21 +01:00
)
2020-11-11 04:39:51 +01:00
extract_parser . add_argument (
2020-11-06 03:08:21 +01:00
" -v " ,
" --verbose " ,
action = " store_true " ,
2020-11-11 04:39:51 +01:00
help = " Display verbuse debugging output " ,
2020-11-06 03:08:21 +01:00
)
2020-11-11 04:39:51 +01:00
extract_parser . add_argument (
2020-11-06 19:40:41 +01:00
" -r " ,
" --write-raw " ,
action = " store_true " ,
2020-11-11 04:39:51 +01:00
help = " Always write raw texture files " ,
2020-11-06 19:40:41 +01:00
)
2020-11-11 04:39:51 +01:00
extract_parser . add_argument (
" -m " ,
2020-11-06 20:57:13 +01:00
" --write-mappings " ,
action = " store_true " ,
2020-11-11 04:39:51 +01:00
help = " Write mapping files to disk " ,
)
2020-11-23 21:56:05 +01:00
extract_parser . add_argument (
" -g " ,
" --generate-mapping-overlays " ,
action = " store_true " ,
help = " Generate overlay images showing mappings " ,
)
2020-11-29 01:47:25 +01:00
extract_parser . add_argument (
" -s " ,
" --split-textures " ,
action = " store_true " ,
help = " Split textures into individual sprites " ,
)
2021-04-11 22:45:40 +02:00
extract_parser . add_argument (
" -b " ,
" --write-binaries " ,
action = " store_true " ,
help = " Write binary SWF files to disk " ,
)
2021-05-10 02:12:54 +02:00
extract_parser . add_argument (
" -y " ,
" --write-bytecode " ,
action = " store_true " ,
help = " Write decompiled bytecode files to disk " ,
)
2020-11-11 04:39:51 +01:00
2021-04-16 01:18:33 +02:00
update_parser = subparsers . add_parser ( ' update ' , help = ' Update relevant textures in a TXP2 container from a directory ' )
2020-11-11 04:39:51 +01:00
update_parser . add_argument (
" file " ,
metavar = " FILE " ,
help = " The file to update " ,
)
update_parser . add_argument (
" dir " ,
metavar = " DIR " ,
help = " Directory to update from " ,
)
update_parser . add_argument (
" -p " ,
" --pretend " ,
action = " store_true " ,
help = " Pretend to update instead of updating " ,
)
update_parser . add_argument (
" -v " ,
" --verbose " ,
action = " store_true " ,
help = " Display verbuse debugging output " ,
2020-11-06 20:57:13 +01:00
)
2020-11-27 18:47:48 +01:00
2021-04-16 01:18:33 +02:00
print_parser = subparsers . add_parser ( ' print ' , help = ' Print the TXP2 container contents as a JSON dictionary ' )
2020-11-27 18:47:48 +01:00
print_parser . add_argument (
" file " ,
metavar = " FILE " ,
help = " The file to print " ,
)
2021-04-24 19:59:36 +02:00
print_parser . add_argument (
" -d " ,
" --decompile-bytecode " ,
action = " store_true " ,
help = " Attempt to decompile and print bytecode instead of printing the raw representation. " ,
)
2021-03-30 06:50:05 +02:00
print_parser . add_argument (
" -v " ,
" --verbose " ,
action = " store_true " ,
help = " Display verbuse debugging output " ,
)
2020-11-27 18:47:48 +01:00
2021-04-16 01:18:33 +02:00
parseafp_parser = subparsers . add_parser ( ' parseafp ' , help = ' Parse a raw AFP/BSI file pair previously extracted from an IFS or TXP2 container ' )
2021-04-03 07:30:19 +02:00
parseafp_parser . add_argument (
2021-04-03 07:27:09 +02:00
" afp " ,
metavar = " AFPFILE " ,
help = " The AFP file to parse " ,
)
2021-04-03 07:30:19 +02:00
parseafp_parser . add_argument (
2021-04-03 07:27:09 +02:00
" bsi " ,
metavar = " BSIFILE " ,
help = " The BSI file to parse " ,
)
2021-04-24 19:59:36 +02:00
parseafp_parser . add_argument (
" -d " ,
" --decompile-bytecode " ,
action = " store_true " ,
help = " Attempt to decompile and print bytecode instead of printing the raw representation. " ,
)
2021-04-03 07:30:19 +02:00
parseafp_parser . add_argument (
" -v " ,
" --verbose " ,
action = " store_true " ,
help = " Display verbuse debugging output " ,
)
2021-05-10 02:12:54 +02:00
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 " ,
)
2021-04-16 01:18:33 +02:00
parsegeo_parser = subparsers . add_parser ( ' parsegeo ' , help = ' Parse a raw GEO file previously extracted from an IFS or TXP2 container ' )
2021-04-03 07:30:19 +02:00
parsegeo_parser . add_argument (
" geo " ,
metavar = " GEOFILE " ,
help = " The GEO file to parse " ,
)
parsegeo_parser . add_argument (
2021-04-03 07:27:09 +02:00
" -v " ,
" --verbose " ,
action = " store_true " ,
help = " Display verbuse debugging output " ,
)
2021-04-16 01:18:33 +02:00
render_parser = subparsers . add_parser ( ' render ' , help = ' Render a particular animation out of a series of SWFs ' )
render_parser . add_argument (
" container " ,
metavar = " CONTAINER " ,
type = str ,
nargs = ' + ' ,
help = " A container file to use for loading SWF data. Can be either a TXP2 or IFS container. " ,
)
render_parser . add_argument (
" --path " ,
metavar = " PATH " ,
type = str ,
required = True ,
2021-05-17 00:19:07 +02:00
help = ' A path to render, specified as " moviename " of the animation to render. Use the " list " command to discover paths in a file. ' ,
2021-04-16 01:18:33 +02:00
)
render_parser . add_argument (
" --output " ,
metavar = " IMAGE " ,
type = str ,
default = " out.gif " ,
2021-04-18 01:32:47 +02:00
help = ' The output file (ending either in .gif, .webp or .png) where the render should be saved. ' ,
2021-04-16 01:18:33 +02:00
)
2021-04-21 03:06:48 +02:00
render_parser . add_argument (
" --background-color " ,
type = str ,
default = None ,
2021-05-16 17:15:06 +02:00
help = " Set the background color of the animation as a comma-separated RGB or RGBA color, overriding a default if present in the SWF. " ,
)
2021-05-16 21:40:06 +02:00
render_parser . add_argument (
" --only-depths " ,
type = str ,
default = None ,
help = " Only render objects on these depth planes. Can provide either a number or a comma-separated list of numbers. " ,
)
2021-05-21 23:31:13 +02:00
render_parser . add_argument (
" --force-aspect-ratio " ,
type = str ,
default = None ,
help = " Force the aspect ratio of the rendered image, as a colon-separated aspect ratio such as 16:9 or 4:3. " ,
)
2021-05-21 23:31:39 +02:00
render_parser . add_argument (
" --scale-width " ,
type = float ,
default = 1.0 ,
2021-05-23 22:30:35 +02:00
help = " Scale the width of the animation by some factor, such as 2.0 or 0.5. " ,
2021-05-21 23:31:39 +02:00
)
render_parser . add_argument (
" --scale-height " ,
type = float ,
default = 1.0 ,
2021-05-23 22:30:35 +02:00
help = " Scale the height of the animation by some factor, such as 2.0 or 0.5. " ,
2021-05-21 23:31:39 +02:00
)
2021-05-16 17:15:06 +02:00
render_parser . add_argument (
" --disable-threads " ,
action = " store_true " ,
help = " Disable multi-threaded rendering. " ,
2021-04-21 03:06:48 +02:00
)
2021-05-23 22:30:35 +02:00
render_parser . add_argument (
" -v " ,
" --verbose " ,
action = " store_true " ,
help = " Display verbuse debugging output " ,
)
2021-04-16 01:18:33 +02:00
2021-04-16 23:08:41 +02:00
list_parser = subparsers . add_parser ( ' list ' , help = ' List out the possible paths to render from a series of SWFs ' )
list_parser . add_argument (
" container " ,
metavar = " CONTAINER " ,
type = str ,
nargs = ' + ' ,
help = " A container file to use for loading SWF data. Can be either a TXP2 or IFS container. " ,
)
list_parser . add_argument (
" -v " ,
" --verbose " ,
action = " store_true " ,
help = " Display verbuse debugging output " ,
)
2021-05-16 17:15:06 +02:00
2020-11-06 03:08:21 +01:00
args = parser . parse_args ( )
2020-11-11 04:39:51 +01:00
if args . action == " extract " :
2020-11-29 01:47:25 +01:00
if args . split_textures :
if args . write_raw :
raise Exception ( " Cannot write raw textures when splitting sprites! " )
if args . generate_mapping_overlays :
raise Exception ( " Cannot generate mapping overlays when splitting sprites! " )
2020-11-11 04:39:51 +01:00
with open ( args . file , " rb " ) as bfp :
2021-04-11 22:44:31 +02:00
afpfile = TXP2File ( bfp . read ( ) , verbose = args . verbose )
2020-11-10 04:26:06 +01:00
2020-11-11 04:39:51 +01:00
# Actually place the files down.
os . makedirs ( args . dir , exist_ok = True )
2020-11-10 04:26:06 +01:00
2020-11-29 01:47:25 +01:00
if not args . split_textures :
for texture in afpfile . textures :
filename = os . path . join ( args . dir , texture . name )
2020-11-10 04:26:06 +01:00
2020-11-29 01:47:25 +01:00
if texture . img :
if args . pretend :
print ( f " Would write { filename } .png texture... " )
else :
print ( f " Writing { filename } .png texture... " )
with open ( f " { filename } .png " , " wb " ) as bfp :
texture . img . save ( bfp , format = ' PNG ' )
2020-11-10 04:26:06 +01:00
2020-11-29 01:47:25 +01:00
if not texture . img or args . write_raw :
if args . pretend :
print ( f " Would write { filename } .raw texture... " )
else :
print ( f " Writing { filename } .raw texture... " )
with open ( f " { filename } .raw " , " wb " ) as bfp :
bfp . write ( texture . raw )
2020-11-11 04:39:51 +01:00
2020-11-29 01:47:25 +01:00
if args . pretend :
print ( f " Would write { filename } .xml texture info... " )
else :
print ( f " Writing { filename } .xml texture info... " )
with open ( f " { filename } .xml " , " w " ) as sfp :
sfp . write ( textwrap . dedent ( f """
< info >
< width > { texture . width } < / width >
< height > { texture . height } < / height >
< type > { hex ( texture . fmt ) } < / type >
< raw > { filename } . raw < / raw >
< / info >
""" ).strip())
2020-11-11 04:39:51 +01:00
if args . write_mappings :
2020-11-29 01:47:25 +01:00
if not args . split_textures :
for i , name in enumerate ( afpfile . regionmap . entries ) :
if i < 0 or i > = len ( afpfile . texture_to_region ) :
raise Exception ( f " Out of bounds region { i } " )
region = afpfile . texture_to_region [ i ]
texturename = afpfile . texturemap . entries [ region . textureno ]
filename = os . path . join ( args . dir , name )
if args . pretend :
print ( f " Would write { filename } .xml region information... " )
else :
print ( f " Writing { filename } .xml region information... " )
with open ( f " { filename } .xml " , " w " ) as sfp :
sfp . write ( textwrap . dedent ( f """
< info >
< left > { region . left } < / left >
< top > { region . top } < / top >
< right > { region . right } < / right >
< bottom > { region . bottom } < / bottom >
< texture > { texturename } < / texture >
< / info >
""" ).strip())
2020-11-10 04:26:06 +01:00
2020-11-11 04:39:51 +01:00
if afpfile . fontdata is not None :
filename = os . path . join ( args . dir , " fontinfo.xml " )
2020-11-10 04:26:06 +01:00
2020-11-11 04:39:51 +01:00
if args . pretend :
print ( f " Would write { filename } font information... " )
else :
print ( f " Writing { filename } font information... " )
with open ( filename , " w " ) as sfp :
sfp . write ( str ( afpfile . fontdata ) )
2021-04-11 22:45:40 +02:00
if args . write_binaries :
for i , name in enumerate ( afpfile . swfmap . entries ) :
swf = afpfile . swfdata [ i ]
filename = os . path . join ( args . dir , name )
if args . pretend :
print ( f " Would write { filename } .afp SWF data... " )
print ( f " Would write { filename } .bsi SWF descramble data... " )
else :
print ( f " Writing { filename } .afp SWF data... " )
with open ( f " { filename } .afp " , " wb " ) as bfp :
bfp . write ( swf . data )
print ( f " Writing { filename } .bsi SWF descramble data... " )
with open ( f " { filename } .bsi " , " wb " ) as bfp :
bfp . write ( swf . descramble_info )
for i , name in enumerate ( afpfile . shapemap . entries ) :
shape = afpfile . shapes [ i ]
filename = os . path . join ( args . dir , f " { name } .geo " )
if args . pretend :
print ( f " Would write { filename } shape data... " )
else :
print ( f " Writing { filename } shape data... " )
with open ( filename , " wb " ) as bfp :
bfp . write ( shape . data )
2020-11-23 21:56:05 +01:00
if args . generate_mapping_overlays :
overlays : Dict [ str , Any ] = { }
for i , name in enumerate ( afpfile . regionmap . entries ) :
if i < 0 or i > = len ( afpfile . texture_to_region ) :
raise Exception ( f " Out of bounds region { i } " )
region = afpfile . texture_to_region [ i ]
texturename = afpfile . texturemap . entries [ region . textureno ]
if texturename not in overlays :
for texture in afpfile . textures :
if texture . name == texturename :
overlays [ texturename ] = Image . new (
' RGBA ' ,
( texture . width , texture . height ) ,
( 0 , 0 , 0 , 0 ) ,
)
break
else :
raise Exception ( f " Couldn ' t find texture { texturename } " )
draw = ImageDraw . Draw ( overlays [ texturename ] )
draw . rectangle (
( ( region . left / / 2 , region . top / / 2 ) , ( region . right / / 2 , region . bottom / / 2 ) ) ,
fill = ( 0 , 0 , 0 , 0 ) ,
outline = ( 255 , 0 , 0 , 255 ) ,
width = 1 ,
)
draw . text (
( region . left / / 2 , region . top / / 2 ) ,
name ,
fill = ( 255 , 0 , 255 , 255 ) ,
)
for name , img in overlays . items ( ) :
filename = os . path . join ( args . dir , name ) + " _overlay.png "
if args . pretend :
print ( f " Would write { filename } overlay... " )
else :
print ( f " Writing { filename } overlay... " )
with open ( filename , " wb " ) as bfp :
img . save ( bfp , format = ' PNG ' )
2020-11-29 01:47:25 +01:00
if args . split_textures :
textures : Dict [ str , Any ] = { }
announced : Dict [ str , bool ] = { }
for i , name in enumerate ( afpfile . regionmap . entries ) :
if i < 0 or i > = len ( afpfile . texture_to_region ) :
raise Exception ( f " Out of bounds region { i } " )
region = afpfile . texture_to_region [ i ]
texturename = afpfile . texturemap . entries [ region . textureno ]
if texturename not in textures :
for tex in afpfile . textures :
if tex . name == texturename :
textures [ texturename ] = tex
break
else :
raise Exception ( " Could not find texture {texturename} to split! " )
if textures [ texturename ] . img :
# Grab the location in the image, save it out to a new file.
filename = f " { texturename } _ { name } .png "
filename = os . path . join ( args . dir , filename )
if args . pretend :
print ( f " Would write { filename } sprite... " )
else :
print ( f " Writing { filename } sprite... " )
sprite = textures [ texturename ] . img . crop (
( region . left / / 2 , region . top / / 2 , region . right / / 2 , region . bottom / / 2 ) ,
)
with open ( filename , " wb " ) as bfp :
sprite . save ( bfp , format = ' PNG ' )
else :
if not announced . get ( texturename , False ) :
print ( f " Cannot extract sprites from { texturename } because it is not a supported format! " )
announced [ texturename ] = True
2021-05-10 02:12:54 +02:00
if args . write_bytecode :
for swf in afpfile . swfdata :
write_bytecode ( swf , args . dir , verbose = args . verbose )
2020-11-29 01:47:25 +01:00
2020-11-11 04:39:51 +01:00
if args . action == " update " :
# First, parse the file out
with open ( args . file , " rb " ) as bfp :
2021-04-11 22:44:31 +02:00
afpfile = TXP2File ( bfp . read ( ) , verbose = args . verbose )
2020-11-11 04:39:51 +01:00
# Now, find any PNG files that match texture names.
for texture in afpfile . textures :
filename = os . path . join ( args . dir , texture . name ) + " .png "
if os . path . isfile ( filename ) :
print ( f " Updating { texture . name } from { filename } ... " )
2020-11-12 06:03:52 +01:00
with open ( filename , " rb " ) as bfp :
afpfile . update_texture ( texture . name , bfp . read ( ) )
2020-11-11 04:39:51 +01:00
2020-11-29 01:48:44 +01:00
# Now, find any PNG files that match a specific sprite.
for i , spritename in enumerate ( afpfile . regionmap . entries ) :
if i < 0 or i > = len ( afpfile . texture_to_region ) :
raise Exception ( f " Out of bounds region { i } " )
region = afpfile . texture_to_region [ i ]
texturename = afpfile . texturemap . entries [ region . textureno ]
# Grab the location in the image to see if it exists.
filename = f " { texturename } _ { spritename } .png "
filename = os . path . join ( args . dir , filename )
if os . path . isfile ( filename ) :
print ( f " Updating { texturename } sprite piece { spritename } from { filename } ... " )
with open ( filename , " rb " ) as bfp :
afpfile . update_sprite ( texturename , spritename , bfp . read ( ) )
2020-11-11 04:39:51 +01:00
# Now, write out the updated file
if args . pretend :
print ( f " Would write { args . file } ... " )
afpfile . unparse ( )
else :
print ( f " Writing { args . file } ... " )
2020-11-24 01:03:28 +01:00
data = afpfile . unparse ( )
2020-11-23 23:33:12 +01:00
with open ( args . file , " wb " ) as bfp :
2020-11-24 01:03:28 +01:00
bfp . write ( data )
2020-11-06 03:08:21 +01:00
2020-11-27 18:47:48 +01:00
if args . action == " print " :
# First, parse the file out
with open ( args . file , " rb " ) as bfp :
2021-04-11 22:44:31 +02:00
afpfile = TXP2File ( bfp . read ( ) , verbose = args . verbose )
2020-11-27 18:47:48 +01:00
# Now, print it
2021-05-10 02:12:28 +02:00
print ( json . dumps ( afpfile . as_dict ( decompile_bytecode = args . decompile_bytecode , verbose = args . verbose ) , sort_keys = True , indent = 4 ) )
2020-11-27 18:47:48 +01:00
2021-04-03 07:30:19 +02:00
if args . action == " parseafp " :
2021-04-03 07:27:09 +02:00
# 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, print it
swf . parse ( verbose = args . verbose )
2021-05-10 02:12:28 +02:00
print ( json . dumps ( swf . as_dict ( decompile_bytecode = args . decompile_bytecode , verbose = args . verbose ) , sort_keys = True , indent = 4 ) )
2021-04-03 07:27:09 +02:00
2021-05-10 02:12:54 +02:00
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 )
2021-04-03 07:30:19 +02:00
if args . action == " parsegeo " :
# First, load the AFP and BSI files
with open ( args . geo , " rb " ) as bfp :
geo = Shape ( " <unnamed> " , bfp . read ( ) )
# Now, print it
geo . parse ( )
if args . verbose :
print ( geo , file = sys . stderr )
print ( json . dumps ( geo . as_dict ( ) , sort_keys = True , indent = 4 ) )
2021-04-16 23:08:41 +02:00
if args . action in [ " render " , " list " ] :
2021-04-16 01:18:33 +02:00
# This is a complicated one, as we need to be able to specify multiple
# directories of files as well as support IFS files and TXP2 files.
2021-05-23 22:31:23 +02:00
if args . action == ' render ' :
renderer = AFPRenderer ( single_threaded = args . disable_threads )
else :
renderer = AFPRenderer ( )
2021-04-16 01:18:33 +02:00
# TODO: Allow specifying individual folders and such.
for container in args . container :
with open ( container , " rb " ) as bfp :
data = bfp . read ( )
afpfile = None
try :
afpfile = TXP2File ( data , verbose = args . verbose )
except Exception :
pass
if afpfile is not None :
2021-04-16 23:08:41 +02:00
if args . verbose :
print ( f " Loading files out of TXP2 container { container } ... " , file = sys . stderr )
# First, load GE2D structures into the renderer.
for i , name in enumerate ( afpfile . shapemap . entries ) :
shape = afpfile . shapes [ i ]
renderer . add_shape ( name , shape )
if args . verbose :
print ( f " Added { name } to SWF shape library. " , file = sys . stderr )
# Now, split and load textures into the renderer.
sheets : Dict [ str , Any ] = { }
for i , name in enumerate ( afpfile . regionmap . entries ) :
if i < 0 or i > = len ( afpfile . texture_to_region ) :
raise Exception ( f " Out of bounds region { i } " )
region = afpfile . texture_to_region [ i ]
texturename = afpfile . texturemap . entries [ region . textureno ]
if texturename not in sheets :
for tex in afpfile . textures :
if tex . name == texturename :
sheets [ texturename ] = tex
break
else :
raise Exception ( " Could not find texture {texturename} to split! " )
if sheets [ texturename ] . img :
sprite = sheets [ texturename ] . img . crop (
( region . left / / 2 , region . top / / 2 , region . right / / 2 , region . bottom / / 2 ) ,
)
renderer . add_texture ( name , sprite )
if args . verbose :
print ( f " Added { name } to SWF texture library. " , file = sys . stderr )
else :
print ( f " Cannot load { name } from { texturename } because it is not a supported format! " )
# Finally, load the SWF data itself into the renderer.
for i , name in enumerate ( afpfile . swfmap . entries ) :
swf = afpfile . swfdata [ i ]
renderer . add_swf ( name , swf )
if args . verbose :
print ( f " Added { name } to SWF library. " , file = sys . stderr )
continue
2021-04-16 01:18:33 +02:00
ifsfile = None
try :
ifsfile = IFS ( data , decode_textures = True )
except Exception :
pass
if ifsfile is not None :
2021-04-16 23:08:41 +02:00
if args . verbose :
print ( f " Loading files out of IFS container { container } ... " , file = sys . stderr )
2021-04-16 01:18:33 +02:00
for fname in ifsfile . filenames :
2021-04-16 05:51:16 +02:00
if fname . startswith ( f " geo { os . sep } " ) :
2021-04-16 01:18:33 +02:00
# Trim off directory.
2021-04-16 05:51:16 +02:00
shapename = fname [ ( 3 + len ( os . sep ) ) : ]
2021-04-16 01:18:33 +02:00
# Load file, register it.
fdata = ifsfile . read_file ( fname )
shape = Shape ( shapename , fdata )
renderer . add_shape ( shapename , shape )
if args . verbose :
print ( f " Added { shapename } to SWF shape library. " , file = sys . stderr )
2021-04-16 05:51:16 +02:00
elif fname . startswith ( f " tex { os . sep } " ) and fname . endswith ( " .png " ) :
2021-04-16 01:18:33 +02:00
# Trim off directory, png extension.
2021-04-16 05:51:16 +02:00
texname = fname [ ( 3 + len ( os . sep ) ) : ] [ : - 4 ]
2021-04-16 01:18:33 +02:00
# Load file, register it.
fdata = ifsfile . read_file ( fname )
tex = Image . open ( io . BytesIO ( fdata ) )
renderer . add_texture ( texname , tex )
if args . verbose :
print ( f " Added { texname } to SWF texture library. " , file = sys . stderr )
2021-04-16 05:51:16 +02:00
elif fname . startswith ( f " afp { os . sep } " ) :
2021-04-16 01:18:33 +02:00
# Trim off directory, see if it has a corresponding bsi.
2021-04-16 05:51:16 +02:00
afpname = fname [ ( 3 + len ( os . sep ) ) : ]
bsipath = f " afp { os . sep } bsi { os . sep } { afpname } "
2021-04-16 01:18:33 +02:00
if bsipath in ifsfile . filenames :
afpdata = ifsfile . read_file ( fname )
bsidata = ifsfile . read_file ( bsipath )
flash = SWF ( afpname , afpdata , bsidata )
renderer . add_swf ( afpname , flash )
if args . verbose :
print ( f " Added { afpname } to SWF library. " , file = sys . stderr )
2021-04-16 23:08:41 +02:00
continue
2021-04-16 01:18:33 +02:00
2021-04-16 23:08:41 +02:00
if args . action == " render " :
2021-04-21 03:06:48 +02:00
# Verify the correct params.
2021-04-18 01:32:47 +02:00
if args . output . lower ( ) . endswith ( " .gif " ) :
fmt = " GIF "
elif args . output . lower ( ) . endswith ( " .webp " ) :
fmt = " WEBP "
elif args . output . lower ( ) . endswith ( " .png " ) :
fmt = " PNG "
else :
raise Exception ( " Unrecognized file extension for output! " )
2021-04-21 03:06:48 +02:00
# Allow overriding background color.
if args . background_color :
colorvals = args . background_color . split ( " , " )
if len ( colorvals ) not in [ 3 , 4 ] :
raise Exception ( " Invalid color, specify a color as a comma-separated RGB or RGBA value! " )
if len ( colorvals ) == 3 :
colorvals . append ( " 255 " )
colorints = [ int ( c . strip ( ) ) for c in colorvals ]
for c in colorints :
if c < 0 or c > 255 :
raise Exception ( " Color values should be between 0 and 255! " )
color = Color ( * [ c / 255.0 for c in colorints ] )
else :
color = None
2021-05-21 23:31:13 +02:00
# Calculate the size of the SWF so we can apply scaling options.
swf_location = renderer . compute_path_location ( args . path )
requested_width = swf_location . width
requested_height = swf_location . height
# Allow overriding the aspect ratio.
if args . force_aspect_ratio :
ratio = args . force_aspect_ratio . split ( " : " )
if len ( ratio ) != 2 :
raise Exception ( " Invalid aspect ratio, specify a ratio such as 16:9 or 4:3! " )
rx , ry = [ float ( r . strip ( ) ) for r in ratio ]
if rx < = 0 or ry < = 0 :
raise Exception ( " Ratio must only include positive numbers! " )
actual_ratio = rx / ry
swf_ratio = swf_location . width / swf_location . height
if abs ( swf_ratio - actual_ratio ) > 0.0001 :
new_width = actual_ratio * swf_location . height
new_height = swf_location . width / actual_ratio
if new_width < swf_location . width and new_height < swf_location . height :
raise Exception ( " Impossible aspect ratio! " )
if new_width > swf_location . width and new_height > swf_location . height :
raise Exception ( " Impossible aspect ratio! " )
# We know that one is larger and one is smaller, pick the larger.
# This way we always stretch instead of shrinking.
if new_width > swf_location . width :
requested_width = new_width
else :
requested_height = new_height
2021-05-21 23:31:39 +02:00
requested_width * = args . scale_width
requested_height * = args . scale_height
2021-05-21 23:31:13 +02:00
# Calculate the overall view matrix based on the requested width/height.
transform = Matrix (
a = requested_width / swf_location . width ,
b = 0.0 ,
c = 0.0 ,
d = requested_height / swf_location . height ,
tx = 0.0 ,
ty = 0.0 ,
)
2021-04-21 03:06:48 +02:00
# Render the gif/webp frames.
2021-05-16 21:40:06 +02:00
if args . only_depths is not None :
depths = [ int ( d . strip ( ) ) for d in args . only_depths . split ( " , " ) ]
else :
depths = None
2021-05-21 23:31:13 +02:00
duration , images = renderer . render_path ( args . path , verbose = args . verbose , background_color = color , only_depths = depths , movie_transform = transform )
2021-05-16 17:15:06 +02:00
2021-04-21 03:06:48 +02:00
if len ( images ) == 0 :
raise Exception ( " Did not render any frames! " )
2021-04-18 01:32:47 +02:00
if fmt in [ " GIF " , " WEBP " ] :
# Write all the frames out in one file.
with open ( args . output , " wb " ) as bfp :
2021-04-20 23:41:09 +02:00
images [ 0 ] . save ( bfp , format = fmt , save_all = True , append_images = images [ 1 : ] , duration = duration , optimize = True )
2021-04-18 01:32:47 +02:00
print ( f " Wrote animation to { args . output } " )
else :
# Write all the frames out in individual_files.
filename = args . output [ : - 4 ]
ext = args . output [ - 4 : ]
2021-05-21 23:47:26 +02:00
# Figure out padding for the images.
frames = len ( images )
if frames > 0 :
digits = f " 0 { int ( math . log10 ( frames ) ) + 1 } "
2021-04-18 01:32:47 +02:00
2021-05-21 23:47:26 +02:00
for i , img in enumerate ( images ) :
fullname = f " { filename } - { i : { digits } } { ext } "
2021-04-18 01:32:47 +02:00
2021-05-21 23:47:26 +02:00
with open ( fullname , " wb " ) as bfp :
img . save ( bfp , format = fmt )
print ( f " Wrote animation frame to { fullname } " )
2021-04-18 01:32:47 +02:00
2021-04-16 23:08:41 +02:00
elif args . action == " list " :
paths = renderer . list_paths ( verbose = args . verbose )
for path in paths :
print ( path )
2021-04-16 01:18:33 +02:00
2020-11-06 03:08:21 +01:00
return 0
if __name__ == " __main__ " :
sys . exit ( main ( ) )