2021-05-23 22:32:21 +02:00
from typing import Any , Dict , List , Tuple , Optional , Union
2021-04-16 01:18:33 +02:00
from PIL import Image # type: ignore
2021-05-16 02:20:23 +02:00
from . blend import affine_composite
2021-05-17 05:55:41 +02:00
from . swf import SWF , Frame , Tag , AP2ShapeTag , AP2DefineSpriteTag , AP2PlaceObjectTag , AP2RemoveObjectTag , AP2DoActionTag , AP2DefineFontTag , AP2DefineEditTextTag , AP2PlaceCameraTag
2021-05-23 22:32:21 +02:00
from . decompile import ByteCode
from . types import Color , Matrix , Point , Rectangle , AP2Trigger , AP2Action , PushAction , StoreRegisterAction , StringConstant , Register , THIS , UNDEFINED
2021-05-11 00:26:26 +02:00
from . geo import Shape , DrawParams
2021-04-16 01:18:33 +02:00
from . util import VerboseOutput
2021-05-11 00:26:26 +02:00
class RegisteredClip :
2021-04-18 01:30:46 +02:00
# A movie clip that we are rendering, frame by frame. These are manifest by the root
2021-04-20 23:41:28 +02:00
# SWF as well as AP2DefineSpriteTags which are essentially embedded movie clips. The
# tag_id is the AP2DefineSpriteTag that created us, or None if this is the clip for
# the root of the movie.
def __init__ ( self , tag_id : Optional [ int ] , frames : List [ Frame ] , tags : List [ Tag ] ) - > None :
2021-04-16 01:18:33 +02:00
self . tag_id = tag_id
self . frames = frames
self . tags = tags
2021-05-11 00:26:26 +02:00
def __repr__ ( self ) - > str :
return f " RegisteredClip(tag_id= { self . tag_id } ) "
2021-04-16 01:18:33 +02:00
2021-05-11 00:26:26 +02:00
class RegisteredShape :
# A shape that we are rendering, as placed by some placed clip somewhere.
def __init__ ( self , tag_id : int , vertex_points : List [ Point ] , tex_points : List [ Point ] , tex_colors : List [ Color ] , draw_params : List [ DrawParams ] ) - > None :
self . tag_id = tag_id
self . vertex_points : List [ Point ] = vertex_points
self . tex_points : List [ Point ] = tex_points
self . tex_colors : List [ Color ] = tex_colors
self . draw_params : List [ DrawParams ] = draw_params
2021-05-21 18:58:39 +02:00
self . rectangle : Optional [ Image . image ] = None
2021-04-18 01:31:08 +02:00
2021-05-11 00:26:26 +02:00
def __repr__ ( self ) - > str :
return f " RegisteredShape(tag_id= { self . tag_id } , vertex_points= { self . vertex_points } , tex_points= { self . tex_points } , tex_colors= { self . tex_colors } , draw_params= { self . draw_params } ) "
2021-05-21 18:58:01 +02:00
class RegisteredDummy :
# An imported tag that we could not find.
def __init__ ( self , tag_id : int ) - > None :
self . tag_id = tag_id
def __repr__ ( self ) - > str :
return f " RegisteredDummy(tag_id= { self . tag_id } ) "
2021-05-11 00:26:26 +02:00
class PlacedObject :
# An object that occupies the screen at some depth.
2021-05-23 22:32:21 +02:00
def __init__ ( self , object_id : int , depth : int , rotation_offset : Point , transform : Matrix , mult_color : Color , add_color : Color , blend : int , mask : Optional [ Rectangle ] ) - > None :
2021-05-11 00:26:26 +02:00
self . __object_id = object_id
self . __depth = depth
self . rotation_offset = rotation_offset
self . transform = transform
self . mult_color = mult_color
self . add_color = add_color
self . blend = blend
2021-05-23 22:32:21 +02:00
self . mask = mask
2021-04-20 23:41:28 +02:00
2021-04-18 01:30:46 +02:00
@property
2021-05-21 18:58:01 +02:00
def source ( self ) - > Union [ RegisteredClip , RegisteredShape , RegisteredDummy ] :
2021-05-11 00:26:26 +02:00
raise NotImplementedError ( " Only implemented in subclass! " )
2021-04-16 01:18:33 +02:00
2021-04-18 01:30:46 +02:00
@property
2021-05-11 00:26:26 +02:00
def depth ( self ) - > int :
return self . __depth
2021-04-18 01:31:08 +02:00
@property
2021-05-11 00:26:26 +02:00
def object_id ( self ) - > int :
return self . __object_id
2021-04-18 01:30:46 +02:00
def __repr__ ( self ) - > str :
2021-05-11 00:26:26 +02:00
return f " PlacedObject(object_id= { self . object_id } , depth= { self . depth } ) "
2021-04-16 01:18:33 +02:00
2021-05-11 00:26:26 +02:00
class PlacedShape ( PlacedObject ) :
# A shape that occupies its parent clip at some depth. Placed by an AP2PlaceObjectTag
# referencing an AP2ShapeTag.
2021-05-23 22:32:21 +02:00
def __init__ (
self ,
object_id : int ,
depth : int ,
rotation_offset : Point ,
transform : Matrix ,
mult_color : Color ,
add_color : Color ,
blend : int ,
mask : Optional [ Rectangle ] ,
source : RegisteredShape ,
) - > None :
super ( ) . __init__ ( object_id , depth , rotation_offset , transform , mult_color , add_color , blend , mask )
2021-05-11 00:26:26 +02:00
self . __source = source
2021-04-16 01:18:33 +02:00
2021-04-18 01:30:46 +02:00
@property
2021-05-11 00:26:26 +02:00
def source ( self ) - > RegisteredShape :
return self . __source
def __repr__ ( self ) - > str :
return f " PlacedShape(object_id= { self . object_id } , depth= { self . depth } , source= { self . source } ) "
class PlacedClip ( PlacedObject ) :
# A movieclip that occupies its parent clip at some depth. Placed by an AP2PlaceObjectTag
# referencing an AP2DefineSpriteTag. Essentially an embedded movie clip.
2021-05-23 22:32:21 +02:00
def __init__ (
self ,
object_id : int ,
depth : int ,
rotation_offset : Point ,
transform : Matrix ,
mult_color : Color ,
add_color : Color ,
blend : int ,
mask : Optional [ Rectangle ] ,
source : RegisteredClip ,
) - > None :
super ( ) . __init__ ( object_id , depth , rotation_offset , transform , mult_color , add_color , blend , mask )
2021-05-11 00:26:26 +02:00
self . placed_objects : List [ PlacedObject ] = [ ]
self . frame : int = 0
self . __source = source
2021-05-23 22:32:21 +02:00
self . playing : bool = True
self . requested_frame : Optional [ int ] = None
2021-04-18 01:30:46 +02:00
@property
2021-05-11 00:26:26 +02:00
def source ( self ) - > RegisteredClip :
return self . __source
def advance ( self ) - > None :
if self . frame < len ( self . source . frames ) :
self . frame + = 1
@property
def finished ( self ) - > bool :
return self . frame == len ( self . source . frames )
2021-04-18 01:30:46 +02:00
def __repr__ ( self ) - > str :
2021-05-17 00:19:07 +02:00
return f " PlacedClip(object_id= { self . object_id } , depth= { self . depth } , source= { self . source } , frame= { self . frame } , total_frames= { len ( self . source . frames ) } , finished= { self . finished } ) "
2021-04-18 01:30:46 +02:00
2021-05-23 22:32:21 +02:00
# The following are attributes and functions necessary to support some simple bytecode.
def gotoAndPlay ( self , frame : Any ) - > None :
if not isinstance ( frame , int ) :
# TODO: Technically this should also allow string labels to frames as identified in the
# SWF specification, but we don't support that here.
print ( f " WARNING: Non-integer frame { frame } to gotoAndPlay function! " )
return
if frame < = 0 or frame > len ( self . source . frames ) :
return
self . requested_frame = frame
self . playing = True
@property
def frameOffset ( self ) - > int :
return self . requested_frame or self . frame
@frameOffset.setter
def frameOffset ( self , val : Any ) - > None :
if not isinstance ( val , int ) :
# TODO: Technically this should also allow string labels to frames as identified in the
# SWF specification, but we don't support that here.
print ( f " WARNING: Non-integer frameOffset { val } to frameOffset attribute! " )
return
if val < 0 or val > = len ( self . source . frames ) :
return
self . requested_frame = val + 1
2021-04-16 01:18:33 +02:00
2021-05-21 18:58:01 +02:00
class PlacedDummy ( PlacedObject ) :
# A reference to an object we can't find because we're missing the import.
2021-05-23 22:32:21 +02:00
def __init__ (
self ,
object_id : int ,
depth : int ,
rotation_offset : Point ,
transform : Matrix ,
mult_color : Color ,
add_color : Color ,
blend : int ,
mask : Optional [ Rectangle ] ,
source : RegisteredDummy ,
) - > None :
super ( ) . __init__ ( object_id , depth , rotation_offset , transform , mult_color , add_color , blend , mask )
2021-05-21 18:58:01 +02:00
self . __source = source
@property
def source ( self ) - > RegisteredDummy :
return self . __source
2021-05-23 22:32:21 +02:00
class Movie :
def __init__ ( self , root : PlacedClip ) - > None :
self . __root = root
def getInstanceAtDepth ( self , depth : Any ) - > Any :
if not isinstance ( depth , int ) :
return UNDEFINED
# For some reason, it looks like internally the depth of all objects is
# stored added to -0x4000, so let's reverse that.
depth = depth + 0x4000
for obj in self . __root . placed_objects :
if obj . depth == depth :
return obj
print ( f " WARNING: Could not find object at depth { depth } ! " )
return UNDEFINED
class AEPLib :
def __init__ ( self , this : PlacedObject , movie : Movie ) - > None :
self . __this = this
self . __movie = movie
def aep_set_rect_mask ( self , thisptr : Any , left : Any , right : Any , top : Any , bottom : Any ) - > None :
if not isinstance ( left , ( int , float ) ) or not isinstance ( right , ( int , float ) ) or not isinstance ( top , ( int , float ) ) or not isinstance ( bottom , ( int , float ) ) :
print ( " WARNING: Ignoring aeplib.aep_set_rect_mask call with invalid parameters! " )
return
if thisptr is THIS :
self . __this . mask = Rectangle (
left = float ( left ) ,
right = float ( right ) ,
top = float ( top ) ,
bottom = float ( bottom ) ,
)
else :
print ( " WARNING: Ignoring aeplib.aep_set_rect_mask call with unrecognized target! " )
def aep_set_set_frame ( self , thisptr : Any , frame : Any ) - > None :
# I have no idea what this should do, so let's ignore it.
pass
2021-04-16 01:18:33 +02:00
class AFPRenderer ( VerboseOutput ) :
2021-05-16 17:15:06 +02:00
def __init__ ( self , shapes : Dict [ str , Shape ] = { } , textures : Dict [ str , Image . Image ] = { } , swfs : Dict [ str , SWF ] = { } , single_threaded : bool = False ) - > None :
2021-04-16 01:18:33 +02:00
super ( ) . __init__ ( )
2021-05-16 17:15:06 +02:00
# Options for rendering
self . __single_threaded = single_threaded
2021-05-21 18:58:01 +02:00
# Library of shapes (draw instructions), textures (actual images) and swfs (us and other files for imports).
2021-04-16 01:18:33 +02:00
self . shapes : Dict [ str , Shape ] = shapes
2021-04-18 01:30:46 +02:00
self . textures : Dict [ str , Image . Image ] = textures
2021-04-16 01:18:33 +02:00
self . swfs : Dict [ str , SWF ] = swfs
2021-05-11 00:26:26 +02:00
# Internal render parameters.
2021-05-21 18:58:01 +02:00
self . __registered_objects : Dict [ int , Union [ RegisteredShape , RegisteredClip , RegisteredDummy ] ] = { }
2021-05-23 22:32:21 +02:00
self . __movie : Optional [ Movie ] = None
2021-04-16 01:18:33 +02:00
def add_shape ( self , name : str , data : Shape ) - > None :
2021-04-18 01:30:46 +02:00
# Register a named shape with the renderer.
2021-04-16 01:18:33 +02:00
if not data . parsed :
data . parse ( )
self . shapes [ name ] = data
2021-04-18 01:30:46 +02:00
def add_texture ( self , name : str , data : Image . Image ) - > None :
# Register a named texture (already loaded PIL image) with the renderer.
2021-04-21 02:01:35 +02:00
self . textures [ name ] = data . convert ( " RGBA " )
2021-04-16 01:18:33 +02:00
def add_swf ( self , name : str , data : SWF ) - > None :
2021-04-18 01:30:46 +02:00
# Register a named SWF with the renderer.
2021-04-16 01:18:33 +02:00
if not data . parsed :
data . parse ( )
self . swfs [ name ] = data
2021-05-21 23:31:13 +02:00
def render_path (
self ,
path : str ,
background_color : Optional [ Color ] = None ,
only_depths : Optional [ List [ int ] ] = None ,
movie_transform : Matrix = Matrix . identity ( ) ,
verbose : bool = False ,
) - > Tuple [ int , List [ Image . Image ] ] :
2021-05-17 00:19:07 +02:00
# Given a path to a SWF root animation, attempt to render it to a list of frames.
2021-04-16 01:18:33 +02:00
for name , swf in self . swfs . items ( ) :
2021-05-17 00:19:07 +02:00
if swf . exported_name == path :
2021-04-16 01:18:33 +02:00
# This is the SWF we care about.
with self . debugging ( verbose ) :
2021-04-21 03:06:48 +02:00
swf . color = background_color or swf . color
2021-05-21 23:31:13 +02:00
return self . __render ( swf , only_depths , movie_transform )
raise Exception ( f ' { path } not found in registered SWFs! ' )
def compute_path_location (
self ,
path : str ,
) - > Rectangle :
# Given a path to a SWF root animation, find its bounding rectangle.
for name , swf in self . swfs . items ( ) :
if swf . exported_name == path :
# This is the SWF we care about.
return swf . location
2021-04-16 01:18:33 +02:00
raise Exception ( f ' { path } not found in registered SWFs! ' )
2021-04-16 23:08:41 +02:00
def list_paths ( self , verbose : bool = False ) - > List [ str ] :
2021-04-18 01:30:46 +02:00
# Given the loaded animations, return a list of possible paths to render.
2021-04-16 23:08:41 +02:00
paths : List [ str ] = [ ]
for name , swf in self . swfs . items ( ) :
paths . append ( swf . exported_name )
return paths
2021-05-23 22:32:21 +02:00
def __execute_bytecode ( self , bytecode : ByteCode , clip : PlacedClip ) - > None :
if self . __movie is None :
raise Exception ( " Logic error, executing bytecode outside of a rendering movie clip! " )
location : int = 0
stack : List [ Any ] = [ ]
variables : Dict [ str , Any ] = {
' aeplib ' : AEPLib ( clip , self . __movie ) ,
' GLOBAL ' : self . __movie ,
}
registers : List [ Any ] = [ UNDEFINED ] * 256
while location < len ( bytecode . actions ) :
action = bytecode . actions [ location ]
if action . opcode == AP2Action . END :
# End the execution.
self . vprint ( " Ending bytecode execution. " )
break
elif action . opcode == AP2Action . GET_VARIABLE :
varname = stack . pop ( )
# Look up the variable, put it on the stack.
if varname in variables :
stack . append ( variables [ varname ] )
else :
stack . append ( UNDEFINED )
elif action . opcode == AP2Action . SET_MEMBER :
# Grab what we're about to do.
set_value = stack . pop ( )
attribute = stack . pop ( )
obj = stack . pop ( )
if not hasattr ( obj , attribute ) :
print ( f " WARNING: Tried to set attribute { attribute } on { obj } but that attribute doesn ' t exist! " )
else :
setattr ( obj , attribute , set_value )
elif action . opcode == AP2Action . CALL_METHOD :
# Grab the method name.
methname = stack . pop ( )
# Grab the object to perform the call on.
obj = stack . pop ( )
# Grab the parameters to pass to the function.
num_params = stack . pop ( )
if not isinstance ( num_params , int ) :
raise Exception ( " Logic error, cannot get number of parameters to method call! " )
params = [ ]
for _ in range ( num_params ) :
params . append ( stack . pop ( ) )
# Look up the python function we're calling.
try :
meth = getattr ( obj , methname )
# Call it, set the return on the stack.
stack . append ( meth ( * params ) )
except AttributeError :
# Function does not exist!
print ( f " WARNING: Tried to call { methname } ( { ' , ' . join ( repr ( s ) for s in params ) } ) on { obj } but that method doesn ' t exist! " )
stack . append ( UNDEFINED )
elif action . opcode == AP2Action . CALL_FUNCTION :
# Grab the method name.
funcname = stack . pop ( )
# Grab the object to perform the call on.
obj = variables [ ' GLOBAL ' ]
# Grab the parameters to pass to the function.
num_params = stack . pop ( )
if not isinstance ( num_params , int ) :
raise Exception ( " Logic error, cannot get number of parameters to function call! " )
params = [ ]
for _ in range ( num_params ) :
params . append ( stack . pop ( ) )
# Look up the python function we're calling.
try :
func = getattr ( obj , funcname )
# Call it, set the return on the stack.
stack . append ( func ( * params ) )
except AttributeError :
# Function does not exist!
print ( f " WARNING: Tried to call { funcname } ( { ' , ' . join ( repr ( s ) for s in params ) } ) on { obj } but that function doesn ' t exist! " )
stack . append ( UNDEFINED )
elif isinstance ( action , PushAction ) :
for obj in action . objects :
if isinstance ( obj , Register ) :
stack . append ( registers [ obj . no ] )
elif isinstance ( obj , StringConstant ) :
if obj . alias :
stack . append ( obj . alias )
else :
stack . append ( StringConstant . property_to_name ( obj . const ) )
else :
stack . append ( obj )
elif isinstance ( action , StoreRegisterAction ) :
set_value = stack . pop ( )
if action . preserve_stack :
stack . append ( set_value )
for reg in action . registers :
registers [ reg . no ] = set_value
elif action . opcode == AP2Action . POP :
stack . pop ( )
else :
print ( f " WARNING: Unhandled opcode { action } with stack { stack } " )
# Next opcode!
location + = 1
2021-05-11 00:26:26 +02:00
def __place ( self , tag : Tag , operating_clip : PlacedClip , prefix : str = " " ) - > Tuple [ Optional [ PlacedClip ] , bool ] :
2021-04-18 01:30:46 +02:00
# "Place" a tag on the screen. Most of the time, this means performing the action of the tag,
# such as defining a shape (registering it with our shape list) or adding/removing an object.
2021-04-16 01:18:33 +02:00
if isinstance ( tag , AP2ShapeTag ) :
2021-05-11 00:26:26 +02:00
self . vprint ( f " { prefix } Loading { tag . reference } into object slot { tag . id } " )
2021-04-16 01:18:33 +02:00
if tag . reference not in self . shapes :
raise Exception ( f " Cannot find shape reference { tag . reference } ! " )
2021-05-11 00:26:26 +02:00
if tag . id in self . __registered_objects :
raise Exception ( f " Cannot register { tag . reference } as object slot { tag . id } is already taken! " )
self . __registered_objects [ tag . id ] = RegisteredShape (
tag . id ,
self . shapes [ tag . reference ] . vertex_points ,
self . shapes [ tag . reference ] . tex_points ,
self . shapes [ tag . reference ] . tex_colors ,
self . shapes [ tag . reference ] . draw_params ,
)
2021-04-16 01:18:33 +02:00
2021-05-11 00:26:26 +02:00
# Didn't place a new clip, didn't change anything.
return None , False
2021-04-18 01:30:46 +02:00
2021-04-16 01:18:33 +02:00
elif isinstance ( tag , AP2DefineSpriteTag ) :
2021-05-11 00:26:26 +02:00
self . vprint ( f " { prefix } Loading Sprite into object slot { tag . id } " )
2021-04-20 23:41:28 +02:00
2021-05-11 00:26:26 +02:00
if tag . id in self . __registered_objects :
raise Exception ( f " Cannot register sprite as object slot { tag . id } is already taken! " )
2021-04-16 01:18:33 +02:00
2021-04-18 01:31:08 +02:00
# Register a new clip that we might reference to execute.
2021-05-11 00:26:26 +02:00
self . __registered_objects [ tag . id ] = RegisteredClip ( tag . id , tag . frames , tag . tags )
# Didn't place a new clip, didn't change anything.
return None , False
2021-04-18 01:31:08 +02:00
2021-04-20 23:41:28 +02:00
elif isinstance ( tag , AP2PlaceObjectTag ) :
2021-04-16 01:18:33 +02:00
if tag . update :
2021-05-15 06:22:43 +02:00
for i in range ( len ( operating_clip . placed_objects ) - 1 , - 1 , - 1 ) :
obj = operating_clip . placed_objects [ i ]
2021-04-16 01:19:56 +02:00
2021-04-18 01:30:46 +02:00
if obj . object_id == tag . object_id and obj . depth == tag . depth :
2021-05-15 06:22:43 +02:00
new_mult_color = tag . mult_color or obj . mult_color
new_add_color = tag . add_color or obj . add_color
new_transform = tag . transform or obj . transform
new_rotation_offset = tag . rotation_offset or obj . rotation_offset
new_blend = tag . blend or obj . blend
if tag . source_tag_id is not None and tag . source_tag_id != obj . source . tag_id :
# This completely updates the pointed-at object.
self . vprint ( f " { prefix } Replacing Object source { obj . source . tag_id } with { tag . source_tag_id } on object with Object ID { tag . object_id } onto Depth { tag . depth } " )
newobj = self . __registered_objects [ tag . source_tag_id ]
if isinstance ( newobj , RegisteredShape ) :
operating_clip . placed_objects [ i ] = PlacedShape (
obj . object_id ,
obj . depth ,
new_rotation_offset ,
new_transform ,
new_mult_color ,
new_add_color ,
new_blend ,
2021-05-23 22:32:21 +02:00
obj . mask ,
2021-05-15 06:22:43 +02:00
newobj ,
)
# Didn't place a new clip, changed the parent clip.
return None , True
elif isinstance ( newobj , RegisteredClip ) :
new_clip = PlacedClip (
tag . object_id ,
tag . depth ,
new_rotation_offset ,
new_transform ,
new_mult_color ,
new_add_color ,
new_blend ,
2021-05-23 22:32:21 +02:00
obj . mask ,
2021-05-15 06:22:43 +02:00
newobj ,
)
operating_clip . placed_objects [ i ] = new_clip
# Placed a new clip, changed the parent.
return new_clip , True
2021-05-21 18:58:01 +02:00
elif isinstance ( newobj , RegisteredDummy ) :
operating_clip . placed_objects [ i ] = PlacedDummy (
obj . object_id ,
obj . depth ,
new_rotation_offset ,
new_transform ,
new_mult_color ,
new_add_color ,
new_blend ,
2021-05-23 22:32:21 +02:00
obj . mask ,
2021-05-21 18:58:01 +02:00
newobj ,
)
# Didn't place a new clip, changed the parent clip.
return None , True
2021-05-15 06:22:43 +02:00
else :
raise Exception ( f " Unrecognized object with Tag ID { tag . source_tag_id } ! " )
else :
# As far as I can tell, pretty much only color and matrix stuff can be updated.
self . vprint ( f " { prefix } Updating Object ID { tag . object_id } on Depth { tag . depth } " )
obj . mult_color = new_mult_color
obj . add_color = new_add_color
obj . transform = new_transform
obj . rotation_offset = new_rotation_offset
obj . blend = new_blend
return None , True
2021-04-20 23:41:28 +02:00
2021-05-11 00:26:26 +02:00
# Didn't place a new clip, did change something.
2021-05-15 06:22:43 +02:00
print ( f " WARNING: Couldn ' t find tag { tag . object_id } on depth { tag . depth } to update! " )
return None , False
2021-04-16 01:18:33 +02:00
else :
2021-04-20 23:41:28 +02:00
if tag . source_tag_id is None :
raise Exception ( " Cannot place a tag with no source ID and no update flags! " )
2021-04-16 01:18:33 +02:00
2021-05-11 00:26:26 +02:00
if tag . source_tag_id in self . __registered_objects :
self . vprint ( f " { prefix } Placing Object { tag . source_tag_id } with Object ID { tag . object_id } onto Depth { tag . depth } " )
newobj = self . __registered_objects [ tag . source_tag_id ]
if isinstance ( newobj , RegisteredShape ) :
operating_clip . placed_objects . append (
PlacedShape (
tag . object_id ,
tag . depth ,
tag . rotation_offset or Point . identity ( ) ,
tag . transform or Matrix . identity ( ) ,
tag . mult_color or Color ( 1.0 , 1.0 , 1.0 , 1.0 ) ,
tag . add_color or Color ( 0.0 , 0.0 , 0.0 , 0.0 ) ,
tag . blend or 0 ,
2021-05-23 22:32:21 +02:00
None ,
2021-05-11 00:26:26 +02:00
newobj ,
)
)
# Didn't place a new clip, changed the parent clip.
return None , True
elif isinstance ( newobj , RegisteredClip ) :
placed_clip = PlacedClip (
tag . object_id ,
tag . depth ,
tag . rotation_offset or Point . identity ( ) ,
tag . transform or Matrix . identity ( ) ,
tag . mult_color or Color ( 1.0 , 1.0 , 1.0 , 1.0 ) ,
tag . add_color or Color ( 0.0 , 0.0 , 0.0 , 0.0 ) ,
tag . blend or 0 ,
2021-05-23 22:32:21 +02:00
None ,
2021-05-11 00:26:26 +02:00
newobj ,
)
operating_clip . placed_objects . append ( placed_clip )
2021-05-23 22:32:21 +02:00
for flags , code in tag . triggers . items ( ) :
if flags & AP2Trigger . ON_LOAD :
for bytecode in code :
self . __execute_bytecode ( bytecode , placed_clip )
else :
print ( " WARNING: Unhandled PLACE_OBJECT trigger with flags {flags} ! " )
2021-05-11 00:26:26 +02:00
# Placed a new clip, changed the parent.
return placed_clip , True
2021-05-21 18:58:01 +02:00
elif isinstance ( newobj , RegisteredDummy ) :
operating_clip . placed_objects . append (
PlacedDummy (
tag . object_id ,
tag . depth ,
tag . rotation_offset or Point . identity ( ) ,
tag . transform or Matrix . identity ( ) ,
tag . mult_color or Color ( 1.0 , 1.0 , 1.0 , 1.0 ) ,
tag . add_color or Color ( 0.0 , 0.0 , 0.0 , 0.0 ) ,
tag . blend or 0 ,
2021-05-23 22:32:21 +02:00
None ,
2021-05-21 18:58:01 +02:00
newobj ,
)
)
# Didn't place a new clip, changed the parent clip.
return None , True
2021-05-11 00:26:26 +02:00
else :
raise Exception ( f " Unrecognized object with Tag ID { tag . source_tag_id } ! " )
2021-04-21 01:19:14 +02:00
raise Exception ( f " Cannot find a shape or sprite with Tag ID { tag . source_tag_id } ! " )
2021-05-11 00:26:26 +02:00
2021-04-16 01:18:33 +02:00
elif isinstance ( tag , AP2RemoveObjectTag ) :
self . vprint ( f " { prefix } Removing Object ID { tag . object_id } from Depth { tag . depth } " )
if tag . object_id != 0 :
# Remove the identified object by object ID and depth.
2021-04-18 01:31:08 +02:00
# Remember removed objects so we can stop any clips.
removed_objects = [
2021-05-11 00:26:26 +02:00
obj for obj in operating_clip . placed_objects
2021-04-18 01:31:08 +02:00
if obj . object_id == tag . object_id and obj . depth == tag . depth
]
2021-04-18 01:30:46 +02:00
2021-04-18 01:31:08 +02:00
# Get rid of the objects that we're removing from the master list.
2021-05-11 00:26:26 +02:00
operating_clip . placed_objects = [
obj for obj in operating_clip . placed_objects
2021-04-18 01:30:46 +02:00
if not ( obj . object_id == tag . object_id and obj . depth == tag . depth )
2021-04-16 01:18:33 +02:00
]
else :
2021-04-18 01:30:46 +02:00
# Remove the last placed object at this depth. The placed objects list isn't
# ordered so much as apppending to the list means the last placed object at a
# depth comes last.
2021-04-21 01:19:14 +02:00
removed_objects = [ ]
2021-05-11 00:26:26 +02:00
for i in range ( len ( operating_clip . placed_objects ) ) :
real_index = len ( operating_clip . placed_objects ) - ( i + 1 )
2021-04-16 01:18:33 +02:00
2021-05-11 00:26:26 +02:00
if operating_clip . placed_objects [ real_index ] . depth == tag . depth :
removed_objects = operating_clip . placed_objects [ real_index : ( real_index + 1 ) ]
operating_clip . placed_objects = operating_clip . placed_objects [ : real_index ] + operating_clip . placed_objects [ ( real_index + 1 ) : ]
2021-04-16 01:18:33 +02:00
break
2021-04-18 01:31:08 +02:00
2021-04-21 01:19:14 +02:00
if not removed_objects :
print ( f " WARNING: Couldn ' t find object to remove by ID { tag . object_id } and depth { tag . depth } ! " )
2021-05-23 22:32:21 +02:00
# TODO: Handle ON_UNLOAD triggers for this object. I don't think I've ever seen one
# on any object so this might be a pedantic request.
2021-05-11 00:26:26 +02:00
# Didn't place a new clip, changed parent clip.
return None , True
2021-04-16 01:20:27 +02:00
elif isinstance ( tag , AP2DoActionTag ) :
2021-05-23 22:32:21 +02:00
self . __execute_bytecode ( tag . bytecode , operating_clip )
2021-05-11 00:26:26 +02:00
# Didn't place a new clip.
return None , False
2021-04-16 23:28:53 +02:00
elif isinstance ( tag , AP2DefineFontTag ) :
print ( " WARNING: Unhandled DEFINE_FONT tag! " )
2021-05-11 00:26:26 +02:00
# Didn't place a new clip.
return None , False
2021-04-16 23:28:53 +02:00
elif isinstance ( tag , AP2DefineEditTextTag ) :
print ( " WARNING: Unhandled DEFINE_EDIT_TEXT tag! " )
2021-05-11 00:26:26 +02:00
# Didn't place a new clip.
return None , False
2021-05-17 05:55:41 +02:00
elif isinstance ( tag , AP2PlaceCameraTag ) :
print ( " WARNING: Unhandled PLACE_CAMERA tag! " )
# Didn't place a new clip.
return None , False
2021-04-16 01:18:33 +02:00
else :
raise Exception ( f " Failed to process tag: { tag } " )
2021-05-16 21:40:06 +02:00
def __render_object (
self ,
img : Image . Image ,
renderable : PlacedObject ,
parent_transform : Matrix ,
2021-05-22 23:52:17 +02:00
parent_mult_color : Color ,
parent_add_color : Color ,
2021-05-22 23:56:28 +02:00
parent_blend : int ,
2021-05-16 21:40:06 +02:00
only_depths : Optional [ List [ int ] ] = None ,
prefix : str = " " ,
) - > Image . Image :
2021-05-16 21:39:25 +02:00
self . vprint ( f " { prefix } Rendering placed object ID { renderable . object_id } from sprite { renderable . source . tag_id } onto Depth { renderable . depth } " )
2021-05-11 00:26:26 +02:00
# Compute the affine transformation matrix for this object.
2021-05-21 23:32:02 +02:00
transform = renderable . transform . multiply ( parent_transform ) . translate ( Point . identity ( ) . subtract ( renderable . rotation_offset ) )
2021-04-16 01:18:33 +02:00
2021-05-22 23:54:22 +02:00
# Calculate blending and blend color if it is present.
2021-05-22 23:52:17 +02:00
mult_color = ( renderable . mult_color or Color ( 1.0 , 1.0 , 1.0 , 1.0 ) ) . multiply ( parent_mult_color )
add_color = ( renderable . add_color or Color ( 0.0 , 0.0 , 0.0 , 0.0 ) ) . multiply ( parent_mult_color ) . add ( parent_add_color )
2021-05-22 23:54:22 +02:00
blend = renderable . blend or 0
2021-05-22 23:56:28 +02:00
if parent_blend not in { 0 , 1 , 2 } and blend in { 0 , 1 , 2 } :
blend = parent_blend
2021-05-22 23:52:17 +02:00
2021-05-23 22:32:21 +02:00
if renderable . mask :
print ( f " WARNING: Unsupported mask Rectangle( { renderable . mask } )! " )
2021-04-20 23:41:28 +02:00
# Render individual shapes if this is a sprite.
2021-05-11 00:26:26 +02:00
if isinstance ( renderable , PlacedClip ) :
2021-05-22 03:30:58 +02:00
if only_depths is not None :
if renderable . depth not in only_depths :
if renderable . depth != - 1 :
# Not on the correct depth plane.
return img
new_only_depths = only_depths
else :
new_only_depths = None
else :
new_only_depths = None
2021-05-22 03:30:28 +02:00
# This is a sprite placement reference. Make sure that we render lower depths
# first, but preserved placed order as well.
depths = set ( obj . depth for obj in renderable . placed_objects )
for depth in sorted ( depths ) :
for obj in renderable . placed_objects :
if obj . depth != depth :
continue
2021-05-22 23:56:28 +02:00
img = self . __render_object ( img , obj , transform , mult_color , add_color , blend , only_depths = new_only_depths , prefix = prefix + " " )
2021-05-11 00:26:26 +02:00
elif isinstance ( renderable , PlacedShape ) :
2021-05-22 03:30:58 +02:00
if only_depths is not None and renderable . depth not in only_depths :
# Not on the correct depth plane.
return img
2021-05-11 00:26:26 +02:00
# This is a shape draw reference.
shape = renderable . source
# Now, render out shapes.
for params in shape . draw_params :
if not ( params . flags & 0x1 ) :
# Not instantiable, don't render.
2021-05-16 17:15:06 +02:00
return img
2021-05-11 00:26:26 +02:00
if params . flags & 0x4 :
# TODO: Need to support blending and UV coordinate colors here.
print ( " WARNING: Unhandled UV coordinate color! " )
texture = None
if params . flags & 0x2 :
# We need to look up the texture for this.
if params . region not in self . textures :
raise Exception ( f " Cannot find texture reference { params . region } ! " )
texture = self . textures [ params . region ]
2021-05-21 18:58:39 +02:00
if params . flags & 0x8 :
# TODO: This texture gets further blended somehow? Not sure this is ever used.
2021-05-22 23:54:22 +02:00
print ( f " WARNING: Unhandled texture blend color { params . blend } ! " )
2021-05-21 18:58:39 +02:00
elif params . flags & 0x8 :
if shape . rectangle is None :
# This is a raw rectangle. Its possible that the number of vertex points is
# not 4, or that the four points in the vertex_points aren't the four corners
# of a rectangle, but let's assume that doesn't happen for now.
2021-05-22 23:54:22 +02:00
if len ( shape . vertex_points ) != 4 :
print ( " WARNING: Unsupported non-rectangle shape! " )
2021-05-21 18:58:39 +02:00
x_points = set ( p . x for p in shape . vertex_points )
y_points = set ( p . y for p in shape . vertex_points )
left = min ( x_points )
right = max ( x_points )
top = min ( y_points )
bottom = max ( y_points )
2021-05-22 23:54:22 +02:00
# Make sure that the four corners are aligned.
bad = False
for point in x_points :
if point not in { left , right } :
bad = True
break
for point in y_points :
if point not in { top , bottom } :
bad = True
break
if bad :
print ( " WARNING: Unsupported non-rectangle shape! " )
2021-05-21 18:58:39 +02:00
shape . rectangle = Image . new ( ' RGBA ' , ( int ( right - left ) , int ( bottom - top ) ) , ( params . blend . as_tuple ( ) ) )
texture = shape . rectangle
2021-05-11 00:26:26 +02:00
2021-05-21 18:58:39 +02:00
if texture is not None :
2021-05-22 23:53:09 +02:00
img = affine_composite ( img , add_color , mult_color , transform , blend , texture , single_threaded = self . __single_threaded )
2021-05-21 18:58:01 +02:00
elif isinstance ( renderable , PlacedDummy ) :
# Nothing to do!
pass
2021-05-11 00:26:26 +02:00
else :
raise Exception ( f " Unknown placed object type to render { renderable } ! " )
2021-04-18 01:31:36 +02:00
2021-05-16 17:15:06 +02:00
return img
2021-05-11 00:26:26 +02:00
def __process_tags ( self , clip : PlacedClip , prefix : str = " " ) - > bool :
self . vprint ( f " { prefix } Handling placed clip { clip . object_id } at depth { clip . depth } " )
# Track whether anything in ourselves or our children changes during this processing.
changed = False
2021-05-23 22:32:21 +02:00
while True :
# See if this clip should actually be played.
if clip . requested_frame is None and not clip . playing :
break
# See if we need to fast forward to a frame.
if clip . requested_frame is not None :
if clip . frame > clip . requested_frame :
# Rewind this clip to the beginning so we can replay until the requested frame.
clip . placed_objects = [ ]
clip . frame = 0
elif clip . frame == clip . requested_frame :
# We played up through the requested frame, we're done!
clip . requested_frame = None
break
# Clips that are part of our own placed objects which we should handle.
child_clips = [ c for c in clip . placed_objects if isinstance ( c , PlacedClip ) ]
# Execute each tag in the frame.
if not clip . finished :
frame = clip . source . frames [ clip . frame ]
tags = clip . source . tags [ frame . start_tag_offset : ( frame . start_tag_offset + frame . num_tags ) ]
for tagno , tag in enumerate ( tags ) :
# Perform the action of this tag.
self . vprint ( f " { prefix } Sprite Tag ID: { clip . source . tag_id } , Current Tag: { frame . start_tag_offset + tagno } , Num Tags: { frame . num_tags } " )
new_clip , clip_changed = self . __place ( tag , clip , prefix = prefix )
changed = changed or clip_changed
# If we create a new movie clip, process it as well for this frame.
if new_clip :
changed = self . __process_tags ( new_clip , prefix = prefix + " " ) or changed
# Now, handle each of the existing clips.
for child in child_clips :
changed = self . __process_tags ( child , prefix = prefix + " " ) or changed
# Now, advance the frame for this clip.
clip . advance ( )
# See if we should bail.
if clip . requested_frame is None :
break
2021-05-11 00:26:26 +02:00
self . vprint ( f " { prefix } Finished handling placed clip { clip . object_id } at depth { clip . depth } " )
# Return if anything was modified.
return changed
2021-05-21 18:58:01 +02:00
def __handle_imports ( self , swf : SWF ) - > Dict [ int , Union [ RegisteredShape , RegisteredClip , RegisteredDummy ] ] :
external_objects : Dict [ int , Union [ RegisteredShape , RegisteredClip , RegisteredDummy ] ] = { }
# Go through, recursively resolve imports for all SWF files.
for tag_id , imp in swf . imported_tags . items ( ) :
for name , other in self . swfs . items ( ) :
if other . exported_name == imp . swf :
# This SWF should have the tag reference.
if imp . tag not in other . exported_tags :
print ( f " WARNING: { swf . exported_name } imports { imp } but that import is not in { other . exported_name } ! " )
external_objects [ tag_id ] = RegisteredDummy ( tag_id )
break
else :
external_objects [ tag_id ] = self . __find_import ( other , other . exported_tags [ imp . tag ] )
break
else :
print ( f " WARNING: { swf . exported_name } imports { imp } but that SWF is not in our library! " )
external_objects [ tag_id ] = RegisteredDummy ( tag_id )
# Fix up tag IDs to point at our local definition of them.
for tid in external_objects :
external_objects [ tid ] . tag_id = tid
# Return our newly populated registered object table containing all imports!
return external_objects
def __find_import ( self , swf : SWF , tag_id : int ) - > Union [ RegisteredShape , RegisteredClip , RegisteredDummy ] :
if tag_id in swf . imported_tags :
external_objects = self . __handle_imports ( swf )
if tag_id not in external_objects :
raise Exception ( f " Logic error, tag ID { tag_id } is an export for { swf . exported_name } but we didn ' t populate it! " )
return external_objects [ tag_id ]
# We need to do a basic placement to find the registered object so we can return it.
root_clip = RegisteredClip (
None ,
swf . frames ,
swf . tags ,
)
tag = self . __find_tag ( root_clip , tag_id )
if tag is None :
print ( f " WARNING: { swf . exported_name } exports { swf . imported_tags [ tag_id ] } but does not manifest an object! " )
return RegisteredDummy ( tag_id )
return tag
def __find_tag ( self , clip : RegisteredClip , tag_id : int ) - > Optional [ Union [ RegisteredShape , RegisteredClip , RegisteredDummy ] ] :
# Fake-execute this clip to find the tag we need to manifest.
for frame in clip . frames :
tags = clip . tags [ frame . start_tag_offset : ( frame . start_tag_offset + frame . num_tags ) ]
for tagno , tag in enumerate ( tags ) :
# Attempt to place any tags.
if isinstance ( tag , AP2ShapeTag ) :
if tag . id == tag_id :
# We need to be able to see this shape to place it.
if tag . reference not in self . shapes :
raise Exception ( f " Cannot find shape reference { tag . reference } ! " )
# This matched, so this is the import.
return RegisteredShape (
tag . id ,
self . shapes [ tag . reference ] . vertex_points ,
self . shapes [ tag . reference ] . tex_points ,
self . shapes [ tag . reference ] . tex_colors ,
self . shapes [ tag . reference ] . draw_params ,
)
elif isinstance ( tag , AP2DefineSpriteTag ) :
new_clip = RegisteredClip ( tag . id , tag . frames , tag . tags )
if tag . id == tag_id :
# This matched, so it is the clip that we want to export.
return new_clip
# Recursively look in this as well.
maybe_tag = self . __find_tag ( new_clip , tag_id )
if maybe_tag is not None :
return maybe_tag
# We didn't find the tag we were after.
return None
2021-05-21 23:31:13 +02:00
def __render ( self , swf : SWF , only_depths : Optional [ List [ int ] ] , movie_transform : Matrix ) - > Tuple [ int , List [ Image . Image ] ] :
2021-05-21 18:58:01 +02:00
# First, let's attempt to resolve imports.
self . __registered_objects = self . __handle_imports ( swf )
2021-05-21 23:31:13 +02:00
# Initialize overall frame advancement stuff.
2021-04-16 01:18:33 +02:00
spf = 1.0 / swf . fps
2021-04-18 01:30:46 +02:00
frames : List [ Image . Image ] = [ ]
2021-04-16 01:18:33 +02:00
frameno : int = 0
2021-04-18 01:31:08 +02:00
2021-05-21 23:31:13 +02:00
# Calculate actual size based on given movie transform.
actual_size = movie_transform . multiply_point ( Point ( swf . location . width , swf . location . height ) ) . as_tuple ( )
2021-05-11 00:26:26 +02:00
# Create a root clip for the movie to play.
root_clip = PlacedClip (
- 1 ,
- 1 ,
Point . identity ( ) ,
2021-05-21 23:32:19 +02:00
Matrix . identity ( ) ,
2021-05-11 00:26:26 +02:00
Color ( 1.0 , 1.0 , 1.0 , 1.0 ) ,
Color ( 0.0 , 0.0 , 0.0 , 0.0 ) ,
0 ,
2021-05-23 22:32:21 +02:00
None ,
2021-05-11 00:26:26 +02:00
RegisteredClip (
None ,
swf . frames ,
swf . tags ,
) ,
)
2021-05-23 22:32:21 +02:00
self . __movie = Movie ( root_clip )
2021-04-16 01:18:33 +02:00
2021-05-22 23:52:17 +02:00
# These could possibly be overwritten from an external source of we wanted.
actual_mult_color = Color ( 1.0 , 1.0 , 1.0 , 1.0 )
actual_add_color = Color ( 0.0 , 0.0 , 0.0 , 0.0 )
2021-05-22 23:56:28 +02:00
actual_blend = 0
2021-05-22 23:52:17 +02:00
2021-05-21 18:58:01 +02:00
# Now play the frames of the root clip.
2021-04-21 02:01:35 +02:00
try :
2021-05-17 00:19:07 +02:00
while not root_clip . finished :
2021-04-21 02:01:35 +02:00
# Create a new image to render into.
2021-05-16 02:18:59 +02:00
time = spf * frameno
2021-04-21 02:01:35 +02:00
color = swf . color or Color ( 0.0 , 0.0 , 0.0 , 0.0 )
2021-05-17 06:04:38 +02:00
self . vprint ( f " Rendering frame { frameno } / { len ( root_clip . source . frames ) } ( { round ( time , 2 ) } s) " )
2021-04-21 02:01:35 +02:00
# Go through all registered clips, place all needed tags.
2021-05-11 00:26:26 +02:00
changed = self . __process_tags ( root_clip )
2021-04-21 02:01:35 +02:00
if changed or frameno == 0 :
2021-05-22 03:30:28 +02:00
# Now, render out the placed objects.
2021-05-21 23:31:13 +02:00
curimage = Image . new ( " RGBA " , actual_size , color = color . as_tuple ( ) )
2021-05-22 23:56:28 +02:00
curimage = self . __render_object ( curimage , root_clip , movie_transform , actual_mult_color , actual_add_color , actual_blend , only_depths = only_depths )
2021-04-21 02:01:35 +02:00
else :
# Nothing changed, make a copy of the previous render.
self . vprint ( " Using previous frame render " )
curimage = frames [ - 1 ] . copy ( )
2021-04-16 01:18:33 +02:00
2021-05-11 00:26:26 +02:00
# Advance our bookkeeping.
2021-04-21 02:01:35 +02:00
frames . append ( curimage )
frameno + = 1
except KeyboardInterrupt :
# Allow ctrl-c to end early and render a partial animation.
2021-05-21 23:31:13 +02:00
print ( f " WARNING: Interrupted early, will render only { len ( frames ) } / { len ( root_clip . source . frames ) } frames of animation! " )
2021-04-18 01:30:46 +02:00
2021-05-23 22:32:21 +02:00
# Clean up
self . movie = None
2021-04-16 01:18:33 +02:00
return int ( spf * 1000.0 ) , frames