2021-05-24 19:38:56 +02:00
from typing import Any , Dict , Generator , List , Set , 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-25 04:01:17 +02:00
from . swf import (
SWF ,
Frame ,
Tag ,
AP2ShapeTag ,
AP2DefineSpriteTag ,
AP2PlaceObjectTag ,
AP2RemoveObjectTag ,
AP2DoActionTag ,
AP2DefineFontTag ,
AP2DefineEditTextTag ,
AP2PlaceCameraTag ,
AP2ImageTag ,
)
2021-05-23 22:32:21 +02:00
from . decompile import ByteCode
2021-05-30 00:11:15 +02:00
from . types import (
Color ,
Matrix ,
Point ,
Rectangle ,
AP2Trigger ,
AP2Action ,
PushAction ,
StoreRegisterAction ,
StringConstant ,
Register ,
NULL ,
UNDEFINED ,
GLOBAL ,
ROOT ,
PARENT ,
THIS ,
CLIP ,
)
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.
2021-05-25 04:01:17 +02:00
def __init__ ( self , tag_id : Optional [ int ] , frames : List [ Frame ] , tags : List [ Tag ] , labels : Dict [ str , int ] ) - > None :
2021-04-16 01:18:33 +02:00
self . tag_id = tag_id
self . frames = frames
self . tags = tags
2021-05-25 04:01:17 +02:00
self . labels = labels
2021-04-16 01:18:33 +02:00
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-25 04:01:17 +02:00
class RegisteredImage :
# An image that we should draw directly.
def __init__ ( self , tag_id : int , reference : str ) - > None :
self . tag_id = tag_id
self . reference = reference
def __repr__ ( self ) - > str :
return f " RegisteredImage(tag_id= { self . tag_id } , reference= { self . reference } ) "
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-23 22:37:18 +02:00
class Mask :
def __init__ ( self , bounds : Rectangle ) - > None :
self . bounds = bounds
self . rectangle : Optional [ Image . Image ] = None
2021-05-11 00:26:26 +02:00
class PlacedObject :
# An object that occupies the screen at some depth.
2021-07-06 23:58:32 +02:00
def __init__ ( self , object_id : int , depth : int , rotation_origin : Point , transform : Matrix , mult_color : Color , add_color : Color , blend : int , mask : Optional [ Mask ] ) - > None :
2021-05-11 00:26:26 +02:00
self . __object_id = object_id
self . __depth = depth
2021-07-06 23:58:32 +02:00
self . rotation_origin = rotation_origin
2021-05-11 00:26:26 +02:00
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-07-30 00:02:10 +02:00
self . visible : bool = True
2021-04-20 23:41:28 +02:00
2021-04-18 01:30:46 +02:00
@property
2021-05-25 04:01:17 +02:00
def source ( self ) - > Union [ RegisteredClip , RegisteredShape , RegisteredImage , 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 ,
2021-07-06 23:58:32 +02:00
rotation_origin : Point ,
2021-05-23 22:32:21 +02:00
transform : Matrix ,
mult_color : Color ,
add_color : Color ,
blend : int ,
2021-05-23 22:37:18 +02:00
mask : Optional [ Mask ] ,
2021-05-23 22:32:21 +02:00
source : RegisteredShape ,
) - > None :
2021-07-06 23:58:32 +02:00
super ( ) . __init__ ( object_id , depth , rotation_origin , 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 ,
2021-07-06 23:58:32 +02:00
rotation_origin : Point ,
2021-05-23 22:32:21 +02:00
transform : Matrix ,
mult_color : Color ,
add_color : Color ,
blend : int ,
2021-05-23 22:37:18 +02:00
mask : Optional [ Mask ] ,
2021-05-23 22:32:21 +02:00
source : RegisteredClip ,
) - > None :
2021-07-06 23:58:32 +02:00
super ( ) . __init__ ( object_id , depth , rotation_origin , transform , mult_color , add_color , blend , mask )
2021-05-11 00:26:26 +02:00
self . placed_objects : List [ PlacedObject ] = [ ]
self . frame : int = 0
2021-05-24 20:35:43 +02:00
self . unplayed_tags : List [ int ] = [ i for i in range ( len ( source . tags ) ) ]
2021-05-11 00:26:26 +02:00
self . __source = source
2021-05-24 03:24:26 +02:00
# Dynamic properties that are adjustable by SWF bytecode.
2021-05-23 22:32:21 +02:00
self . playing : bool = True
self . requested_frame : Optional [ int ] = None
2021-07-30 00:02:10 +02:00
self . visible_frame : int = - 1
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
2021-07-30 00:02:10 +02:00
if self . frame > = self . visible_frame :
self . visible = True
self . visible_frame = - 1
2021-05-11 00:26:26 +02:00
2021-05-24 20:35:43 +02:00
def rewind ( self ) - > None :
self . frame = 0
self . unplayed_tags = [ i for i in range ( len ( self . __source . tags ) ) ]
self . placed_objects = [ ]
2021-05-11 00:26:26 +02:00
@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-06-12 19:17:02 +02:00
return (
f " PlacedClip(object_id= { self . object_id } , depth= { self . depth } , source= { self . source } , frame= { self . frame } , " +
f " requested_frame= { self . requested_frame } , total_frames= { len ( self . source . frames ) } , playing= { self . playing } , " +
f " finished= { self . finished } ) "
)
2021-04-18 01:30:46 +02:00
2021-05-25 04:01:17 +02:00
def __resolve_frame ( self , frame : Any ) - > Optional [ int ] :
if isinstance ( frame , int ) :
return frame
if isinstance ( frame , str ) :
if frame in self . __source . labels :
return self . __source . labels [ frame ]
return None
2021-05-23 22:32:21 +02:00
# The following are attributes and functions necessary to support some simple bytecode.
2021-05-23 22:33:05 +02:00
def gotoAndStop ( self , frame : Any ) - > None :
2021-05-25 04:01:17 +02:00
actual_frame = self . __resolve_frame ( frame )
if actual_frame is None :
print ( f " WARNING: Unrecognized frame { frame } to gotoAndStop function! " )
2021-05-23 22:33:05 +02:00
return
2021-05-25 04:01:17 +02:00
if actual_frame < = 0 or actual_frame > len ( self . source . frames ) :
2021-05-23 22:33:05 +02:00
return
2021-05-25 04:01:17 +02:00
self . requested_frame = actual_frame
2021-05-23 22:33:05 +02:00
self . playing = False
2021-05-23 22:32:21 +02:00
def gotoAndPlay ( self , frame : Any ) - > None :
2021-05-25 04:01:17 +02:00
actual_frame = self . __resolve_frame ( frame )
if actual_frame is None :
2021-05-23 22:32:21 +02:00
print ( f " WARNING: Non-integer frame { frame } to gotoAndPlay function! " )
return
2021-05-25 04:01:17 +02:00
if actual_frame < = 0 or actual_frame > len ( self . source . frames ) :
2021-05-23 22:32:21 +02:00
return
2021-05-25 04:01:17 +02:00
self . requested_frame = actual_frame
2021-05-23 22:32:21 +02:00
self . playing = True
2021-05-23 22:33:05 +02:00
def stop ( self ) - > None :
self . playing = False
def play ( self ) - > None :
self . playing = True
2021-07-30 00:02:10 +02:00
def setInvisibleUntil ( self , frame : Any ) - > None :
actual_frame = self . __resolve_frame ( frame )
if actual_frame is None :
print ( f " WARNING: Non-integer frame { frame } to setInvisibleUntil function! " )
return
self . visible = False
if actual_frame < = 0 or actual_frame > len ( self . source . frames ) :
return
self . visible_frame = actual_frame
2021-05-23 22:32:21 +02:00
@property
def frameOffset ( self ) - > int :
return self . requested_frame or self . frame
@frameOffset.setter
def frameOffset ( self , val : Any ) - > None :
2021-06-12 19:17:02 +02:00
actual_frame = self . __resolve_frame ( val )
if actual_frame is None :
2021-05-23 22:32:21 +02:00
print ( f " WARNING: Non-integer frameOffset { val } to frameOffset attribute! " )
return
2021-06-12 19:17:02 +02:00
if actual_frame < 0 or actual_frame > = len ( self . source . frames ) :
2021-05-23 22:32:21 +02:00
return
2021-06-12 19:17:02 +02:00
self . requested_frame = actual_frame + 1
2021-05-23 22:32:21 +02:00
2021-04-16 01:18:33 +02:00
2021-05-25 04:01:17 +02:00
class PlacedImage ( PlacedObject ) :
# An image that occupies its parent clip at some depth. Placed by an AP2PlaceObjectTag
# referencing an AP2ImageTag.
def __init__ (
self ,
object_id : int ,
depth : int ,
2021-07-06 23:58:32 +02:00
rotation_origin : Point ,
2021-05-25 04:01:17 +02:00
transform : Matrix ,
mult_color : Color ,
add_color : Color ,
blend : int ,
mask : Optional [ Mask ] ,
source : RegisteredImage ,
) - > None :
2021-07-06 23:58:32 +02:00
super ( ) . __init__ ( object_id , depth , rotation_origin , transform , mult_color , add_color , blend , mask )
2021-05-25 04:01:17 +02:00
self . __source = source
@property
def source ( self ) - > RegisteredImage :
return self . __source
def __repr__ ( self ) - > str :
return f " PlacedImage(object_id= { self . object_id } , depth= { self . depth } , source= { self . source } ) "
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 ,
2021-07-06 23:58:32 +02:00
rotation_origin : Point ,
2021-05-23 22:32:21 +02:00
transform : Matrix ,
mult_color : Color ,
add_color : Color ,
blend : int ,
2021-05-23 22:37:18 +02:00
mask : Optional [ Mask ] ,
2021-05-23 22:32:21 +02:00
source : RegisteredDummy ,
) - > None :
2021-07-06 23:58:32 +02:00
super ( ) . __init__ ( object_id , depth , rotation_origin , 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-24 19:36:57 +02:00
class Global :
def __init__ ( self , root : PlacedClip , clip : PlacedClip ) - > None :
2021-05-23 22:37:18 +02:00
self . root = root
2021-05-24 19:36:57 +02:00
self . clip = clip
2021-05-23 22:32:21 +02:00
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
2021-05-24 19:36:57 +02:00
for obj in self . clip . placed_objects :
2021-05-23 22:32:21 +02:00
if obj . depth == depth :
return obj
print ( f " WARNING: Could not find object at depth { depth } ! " )
return UNDEFINED
2021-05-24 19:36:57 +02:00
def __find_parent ( self , parent : PlacedClip , child : PlacedClip ) - > Optional [ PlacedClip ] :
for obj in parent . placed_objects :
if obj is child :
# This is us, so the parent is our parent.
return parent
if isinstance ( obj , PlacedClip ) :
maybe_parent = self . __find_parent ( obj , child )
if maybe_parent is not None :
return maybe_parent
return None
def find_parent ( self , child : PlacedClip ) - > Optional [ PlacedClip ] :
return self . __find_parent ( self . root , child )
2021-05-23 22:32:21 +02:00
class AEPLib :
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 ) ) :
2021-05-23 22:37:18 +02:00
print ( f " WARNING: Ignoring aeplib.aep_set_rect_mask call with invalid parameters { left } , { right } , { top } , { bottom } ! " )
2021-05-23 22:32:21 +02:00
return
2021-05-23 22:37:18 +02:00
if isinstance ( thisptr , PlacedObject ) :
thisptr . mask = Mask (
Rectangle (
left = float ( left ) ,
right = float ( right ) ,
top = float ( top ) ,
bottom = float ( bottom ) ,
) ,
2021-05-23 22:32:21 +02:00
)
else :
2021-05-23 22:37:18 +02:00
print ( f " WARNING: Ignoring aeplib.aep_set_rect_mask call with unrecognized target { thisptr } ! " )
2021-05-23 22:32:21 +02:00
def aep_set_set_frame ( self , thisptr : Any , frame : Any ) - > None :
2021-06-12 19:17:02 +02:00
# This appears to be some sort of callback that the game or other animations can use to figure out
# what frame of animation is currently happening. Whenever I've seen it, it is with the 'frame' set
# to an integer value that matches the currently rendering frame in the render loop. I think its
# safe to ignore this, but if we ever create animations it might be necessary to add calls to this.
2021-05-23 22:32:21 +02:00
pass
2021-06-12 19:17:02 +02:00
def aep_set_frame_control ( self , thisptr : Any , depth : Any , frame : Any ) - > None :
if not isinstance ( thisptr , PlacedClip ) :
print ( f " WARNING: Ignoring aeplib.aep_set_frame_control with unrecognized current object { thisptr } ! " )
return
for obj in thisptr . placed_objects :
if obj . depth == depth :
if not isinstance ( obj , PlacedClip ) :
print ( f " WARNING: Ignoring aeplib.aep_set_frame_control called on object { obj } at depth { depth } ! " )
return
2021-07-30 00:02:10 +02:00
obj . setInvisibleUntil ( frame )
2021-06-12 19:17:02 +02:00
return
print ( f " WARNING: Ignoring aeplib.aep_set_frame_control called on nonexistent object at depth { depth } ! " )
2021-05-25 04:01:17 +02:00
def gotoAndPlay ( self , thisptr : Any , frame : Any ) - > Any :
# This appears to be a wrapper to allow calling gotoAndPlay on clips.
try :
meth = getattr ( thisptr , ' gotoAndPlay ' )
# Call it, set the return on the stack.
return meth ( frame )
except AttributeError :
# Function does not exist!
2021-05-29 02:21:42 +02:00
print ( f " WARNING: Tried to call gotoAndPlay( { frame } ) on { thisptr } but that method doesn ' t exist! " )
return UNDEFINED
def deepGotoAndPlay ( self , thisptr : Any , frame : Any ) - > Any :
# I don't know how this differs from regular gotoAndPlay.
try :
meth = getattr ( thisptr , ' gotoAndPlay ' )
# Call it, set the return on the stack.
return meth ( frame )
except AttributeError :
# Function does not exist!
print ( f " WARNING: Tried to call gotoAndPlay( { frame } ) on { thisptr } but that method doesn ' t exist! " )
return UNDEFINED
def stop ( self , thisptr : Any ) - > Any :
# This appears to be a wrapper to allow calling stop on clips.
try :
meth = getattr ( thisptr , ' stop ' )
# Call it, set the return on the stack.
return meth ( )
except AttributeError :
# Function does not exist!
print ( f " WARNING: Tried to call stop() on { thisptr } but that method doesn ' t exist! " )
2021-05-25 04:01:17 +02:00
return UNDEFINED
2021-05-23 22:32:21 +02:00
2021-05-24 19:37:13 +02:00
class ASDLib :
def sound_play ( self , sound : Any ) - > None :
if not isinstance ( sound , str ) :
print ( f " WARNING: Ignoring asdlib.sound_play call with invalid parameters { sound } ! " )
print ( f " WARNING: Requested sound { sound } be played but we don ' t support sound yet! " )
2021-05-23 22:37:18 +02:00
MissingThis = object ( )
2021-04-16 01:18:33 +02:00
class AFPRenderer ( VerboseOutput ) :
2021-05-31 20:10:25 +02:00
def __init__ ( self , shapes : Dict [ str , Shape ] = { } , textures : Dict [ str , Image . Image ] = { } , swfs : Dict [ str , SWF ] = { } , single_threaded : bool = False , enable_aa : 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-30 06:16:25 +02:00
self . __enable_aa = enable_aa
2021-05-16 17:15:06 +02:00
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-25 04:01:17 +02:00
self . __registered_objects : Dict [ int , Union [ RegisteredShape , RegisteredClip , RegisteredImage , RegisteredDummy ] ] = { }
2021-05-24 19:36:57 +02:00
self . __root : Optional [ PlacedClip ] = None
2021-04-16 01:18:33 +02:00
2021-05-24 19:38:56 +02:00
# List of imports that we provide stub implementations for.
self . __stubbed_swfs : Set [ str ] = {
' aeplib.aeplib ' ,
' aeplib.__Packages.aeplib ' ,
}
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 ,
2021-05-24 01:37:05 +02:00
background_image : Optional [ Image . Image ] = None ,
2021-05-21 23:31:13 +02:00
only_depths : Optional [ List [ int ] ] = None ,
2021-05-25 04:01:36 +02:00
only_frames : Optional [ List [ int ] ] = None ,
2021-05-21 23:31:13 +02:00
movie_transform : Matrix = Matrix . identity ( ) ,
verbose : bool = False ,
2021-05-24 19:36:34 +02:00
) - > Generator [ Image . Image , None , None ] :
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-07-07 00:29:41 +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-25 04:01:36 +02:00
yield from self . __render ( swf , only_depths , only_frames , movie_transform , background_image )
2021-05-24 19:36:34 +02:00
return
2021-05-21 23:31:13 +02:00
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.
2021-07-07 00:29:41 +02:00
for _name , swf in self . swfs . items ( ) :
2021-05-21 23:31:13 +02:00
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-05-24 19:36:34 +02:00
def compute_path_frames (
self ,
path : str ,
) - > int :
# Given a path to a SWF root animation, figure out how many frames are
# in that root path with no regard to bytecode 'stop()' commands.
2021-07-07 00:29:41 +02:00
for _name , swf in self . swfs . items ( ) :
2021-05-24 19:36:34 +02:00
if swf . exported_name == path :
# This is the SWF we care about.
return len ( swf . frames )
raise Exception ( f ' { path } not found in registered SWFs! ' )
2021-04-16 23:08:41 +02:00
2021-05-24 19:36:34 +02:00
def compute_path_frame_duration (
self ,
path : str ,
) - > int :
# Given a path to a SWF root animation, figure out how many milliseconds are
# occupied by each frame.
2021-07-07 00:29:41 +02:00
for _name , swf in self . swfs . items ( ) :
2021-05-24 19:36:34 +02:00
if swf . exported_name == path :
# This is the SWF we care about.
spf = 1.0 / swf . fps
return int ( spf * 1000.0 )
raise Exception ( f ' { path } not found in registered SWFs! ' )
2021-04-16 23:08:41 +02:00
2021-05-29 05:41:25 +02:00
def compute_path_size (
self ,
path : str ,
) - > Rectangle :
# Given a path to a SWF root animation, figure out what the dimensions
# of the SWF are.
2021-07-07 00:29:41 +02:00
for _name , swf in self . swfs . items ( ) :
2021-05-29 05:41:25 +02:00
if swf . exported_name == path :
return swf . location
raise Exception ( f ' { path } not found in registered SWFs! ' )
2021-05-24 19:36:34 +02:00
def list_paths ( self , verbose : bool = False ) - > Generator [ str , None , None ] :
# Given the loaded animations, return a list of possible paths to render.
2021-07-07 00:29:41 +02:00
for _name , swf in self . swfs . items ( ) :
2021-05-24 19:36:34 +02:00
yield swf . exported_name
2021-04-16 23:08:41 +02:00
2021-05-24 03:24:26 +02:00
def __execute_bytecode ( self , bytecode : ByteCode , clip : PlacedClip , thisptr : Optional [ Any ] = MissingThis , prefix : str = " " ) - > None :
2021-05-24 19:36:57 +02:00
if self . __root is None :
2021-05-23 22:32:21 +02:00
raise Exception ( " Logic error, executing bytecode outside of a rendering movie clip! " )
2021-05-24 19:36:57 +02:00
thisobj = clip if ( thisptr is MissingThis ) else thisptr
globalobj = Global ( self . __root , clip )
2021-05-23 22:32:21 +02:00
location : int = 0
stack : List [ Any ] = [ ]
variables : Dict [ str , Any ] = {
2021-05-23 22:37:18 +02:00
' aeplib ' : AEPLib ( ) ,
2021-05-24 19:37:13 +02:00
' asdlib ' : ASDLib ( ) ,
2021-05-23 22:32:21 +02:00
}
registers : List [ Any ] = [ UNDEFINED ] * 256
2021-05-24 03:24:26 +02:00
self . vprint ( f " { prefix } Bytecode engine starting. " )
2021-05-23 22:32:21 +02:00
while location < len ( bytecode . actions ) :
action = bytecode . actions [ location ]
if action . opcode == AP2Action . END :
# End the execution.
2021-05-24 03:24:26 +02:00
self . vprint ( f " { prefix } Ending bytecode execution. " )
2021-05-23 22:32:21 +02:00
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 :
2021-05-29 02:00:15 +02:00
self . vprint ( f " { prefix } Setting attribute { attribute } on { obj } to { set_value } " )
2021-05-23 22:32:21 +02:00
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 :
2021-05-29 02:00:15 +02:00
self . vprint ( f " { prefix } Calling method { methname } ( { ' , ' . join ( repr ( s ) for s in params ) } ) on { obj } " )
2021-05-23 22:32:21 +02:00
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 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 :
2021-05-29 02:00:15 +02:00
self . vprint ( f " { prefix } Calling global function { funcname } ( { ' , ' . join ( repr ( s ) for s in params ) } ) " )
2021-05-24 19:36:57 +02:00
func = getattr ( globalobj , funcname )
2021-05-23 22:32:21 +02:00
# Call it, set the return on the stack.
stack . append ( func ( * params ) )
except AttributeError :
# Function does not exist!
2021-05-24 19:36:57 +02:00
print ( f " WARNING: Tried to call { funcname } ( { ' , ' . join ( repr ( s ) for s in params ) } ) on { globalobj } but that function doesn ' t exist! " )
2021-05-23 22:32:21 +02:00
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 ) )
2021-05-23 22:37:18 +02:00
elif obj is NULL :
stack . append ( None )
2021-05-23 22:33:05 +02:00
elif obj is THIS :
2021-05-24 19:36:57 +02:00
stack . append ( thisobj )
2021-05-23 22:33:05 +02:00
elif obj is GLOBAL :
2021-05-24 19:36:57 +02:00
stack . append ( globalobj )
2021-05-23 22:37:18 +02:00
elif obj is ROOT :
2021-05-24 19:36:57 +02:00
stack . append ( self . __root )
2021-05-23 22:37:18 +02:00
elif obj is CLIP :
# I am not sure this is correct? Maybe it works out
# in circumstances where "THIS" is pointed at something
# else, such as defined function calls maybe?
stack . append ( clip )
elif obj is PARENT :
# Find the parent of this clip.
2021-05-24 19:36:57 +02:00
stack . append ( globalobj . find_parent ( clip ) or UNDEFINED )
2021-05-23 22:32:21 +02:00
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-24 03:24:26 +02:00
self . vprint ( f " { prefix } Bytecode engine finished. " )
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
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-05-25 04:01:17 +02:00
elif isinstance ( tag , AP2ImageTag ) :
self . vprint ( f " { prefix } Loading { tag . reference } into object slot { tag . id } " )
if tag . reference not in self . textures :
raise Exception ( f " Cannot find texture reference { tag . reference } ! " )
self . __registered_objects [ tag . id ] = RegisteredImage (
tag . id ,
tag . reference ,
)
# Didn't place a new clip, didn't change anything.
return None , False
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-04-18 01:31:08 +02:00
# Register a new clip that we might reference to execute.
2021-05-25 04:01:17 +02:00
self . __registered_objects [ tag . id ] = RegisteredClip ( tag . id , tag . frames , tag . tags , tag . labels )
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:31:08 +02:00
2021-04-20 23:41:28 +02:00
elif isinstance ( tag , AP2PlaceObjectTag ) :
2021-07-30 00:02:10 +02:00
if tag . unrecognized_options :
if tag . source_tag_id is not None :
print ( f " WARNING: Place object tag referencing { tag . source_tag_id } includes unparsed options and might not display properly! " )
else :
print ( f " WARNING: Place object tag on depth { tag . depth } includes unparsed options and might not display properly! " )
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
2021-07-06 23:58:32 +02:00
new_rotation_origin = tag . rotation_origin or obj . rotation_origin
2021-05-15 06:22:43 +02:00
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 ,
2021-07-06 23:58:32 +02:00
new_rotation_origin ,
2021-05-15 06:22:43 +02:00
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 ,
)
2021-05-25 04:01:17 +02:00
# Didn't place a new clip, changed the parent clip.
return None , True
elif isinstance ( newobj , RegisteredImage ) :
operating_clip . placed_objects [ i ] = PlacedImage (
obj . object_id ,
obj . depth ,
2021-07-06 23:58:32 +02:00
new_rotation_origin ,
2021-05-25 04:01:17 +02:00
new_transform ,
new_mult_color ,
new_add_color ,
new_blend ,
obj . mask ,
newobj ,
)
2021-05-15 06:22:43 +02:00
# 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 ,
2021-07-06 23:58:32 +02:00
new_rotation_origin ,
2021-05-15 06:22:43 +02:00
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 ,
2021-07-06 23:58:32 +02:00
new_rotation_origin ,
2021-05-21 18:58:01 +02:00
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
2021-07-06 23:58:32 +02:00
obj . rotation_origin = new_rotation_origin
2021-05-15 06:22:43 +02:00
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 ,
2021-07-06 23:58:32 +02:00
tag . rotation_origin or Point . identity ( ) ,
2021-05-11 00:26:26 +02:00
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 ,
)
)
2021-05-25 04:01:17 +02:00
# Didn't place a new clip, changed the parent clip.
return None , True
elif isinstance ( newobj , RegisteredImage ) :
operating_clip . placed_objects . append (
PlacedImage (
tag . object_id ,
tag . depth ,
2021-07-06 23:58:32 +02:00
tag . rotation_origin or Point . identity ( ) ,
2021-05-25 04:01:17 +02:00
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 ,
None ,
newobj ,
)
)
2021-05-11 00:26:26 +02:00
# 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 ,
2021-07-06 23:58:32 +02:00
tag . rotation_origin or Point . identity ( ) ,
2021-05-11 00:26:26 +02:00
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 :
2021-05-24 03:24:26 +02:00
self . __execute_bytecode ( bytecode , placed_clip , prefix = prefix + " " )
2021-05-23 22:32:21 +02:00
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 ,
2021-07-06 23:58:32 +02:00
tag . rotation_origin or Point . identity ( ) ,
2021-05-21 18:58:01 +02:00
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-24 03:24:26 +02:00
self . vprint ( f " { prefix } Execution action tag. " )
self . __execute_bytecode ( tag . bytecode , operating_clip , prefix = prefix + " " )
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-23 22:37:18 +02:00
def __apply_mask (
self ,
parent_mask : Image . Image ,
transform : Matrix ,
mask : Mask ,
) - > Image . Image :
if mask . rectangle is None :
# Calculate the new mask rectangle.
mask . rectangle = Image . new ( ' RGBA ' , ( int ( mask . bounds . width ) , int ( mask . bounds . height ) ) , ( 255 , 0 , 0 , 255 ) )
# Offset it by its top/left.
transform = transform . translate ( Point ( mask . bounds . left , mask . bounds . top ) )
# Draw the mask onto a new image.
calculated_mask = affine_composite (
Image . new ( ' RGBA ' , ( parent_mask . width , parent_mask . height ) , ( 0 , 0 , 0 , 0 ) ) ,
Color ( 0.0 , 0.0 , 0.0 , 0.0 ) ,
Color ( 1.0 , 1.0 , 1.0 , 1.0 ) ,
transform ,
None ,
257 ,
mask . rectangle ,
single_threaded = self . __single_threaded ,
2021-05-30 06:16:25 +02:00
enable_aa = False ,
2021-05-23 22:37:18 +02:00
)
# Composite it onto the current mask.
return affine_composite (
parent_mask . copy ( ) ,
Color ( 0.0 , 0.0 , 0.0 , 0.0 ) ,
Color ( 1.0 , 1.0 , 1.0 , 1.0 ) ,
Matrix . identity ( ) ,
None ,
256 ,
calculated_mask ,
single_threaded = self . __single_threaded ,
2021-05-30 06:16:25 +02:00
enable_aa = False ,
2021-05-23 22:37:18 +02:00
)
2021-05-16 21:40:06 +02:00
def __render_object (
self ,
img : Image . Image ,
renderable : PlacedObject ,
parent_transform : Matrix ,
2021-05-23 22:37:18 +02:00
parent_mask : Image . Image ,
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-07-30 00:02:10 +02:00
if not renderable . visible :
self . vprint ( f " { prefix } Ignoring invisible placed object ID { renderable . object_id } from sprite { renderable . source . tag_id } on Depth { renderable . depth } " )
return img
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-07-06 23:58:32 +02:00
transform = renderable . transform . multiply ( parent_transform ) . translate ( Point . identity ( ) . subtract ( renderable . rotation_origin ) )
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 :
2021-05-23 22:37:18 +02:00
mask = self . __apply_mask ( parent_mask , transform , renderable . mask )
else :
mask = parent_mask
2021-05-23 22:32:21 +02:00
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-31 20:13:43 +02:00
new_only_depths : Optional [ List [ int ] ] = None
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
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-23 22:37:18 +02:00
img = self . __render_object ( img , obj , transform , mask , 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-31 20:13:43 +02:00
if params . blend is None :
raise Exception ( " Logic error, rectangle without a blend color! " )
2021-05-22 23:54:22 +02:00
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-06-13 17:34:30 +02:00
img = affine_composite ( img , add_color , mult_color , transform , mask , blend , texture , single_threaded = self . __single_threaded , enable_aa = self . __enable_aa )
2021-05-25 04:01:17 +02:00
elif isinstance ( renderable , PlacedImage ) :
if only_depths is not None and renderable . depth not in only_depths :
# Not on the correct depth plane.
return img
# This is a shape draw reference.
texture = self . textures [ renderable . source . reference ]
2021-05-30 06:16:25 +02:00
img = affine_composite ( img , add_color , mult_color , transform , mask , blend , texture , single_threaded = self . __single_threaded , enable_aa = self . __enable_aa )
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-24 03:24:26 +02:00
def __is_dirty ( self , clip : PlacedClip ) - > bool :
# If we are dirty ourselves, then the clip is definitely dirty.
if clip . requested_frame is not None :
return True
# If one of our children is dirty, then we are dirty.
for child in clip . placed_objects :
if isinstance ( child , PlacedClip ) :
if self . __is_dirty ( child ) :
return True
# None of our children (or their children, etc...) or ourselves is dirty.
return False
def __process_tags ( self , clip : PlacedClip , only_dirty : bool , prefix : str = " " ) - > bool :
self . vprint ( f " { prefix } Handling { ' dirty updates on ' if only_dirty else ' ' } placed clip { clip . object_id } at depth { clip . depth } " )
2021-05-11 00:26:26 +02:00
# Track whether anything in ourselves or our children changes during this processing.
changed = False
2021-05-24 03:24:26 +02:00
# Make sure to set the requested frame if it isn't set by an external force.
if clip . requested_frame is None :
if not clip . playing or clip . finished or only_dirty :
# We aren't playing this clip because its either paused or finished,
# or it isn't dirty and we're doing dirty updates only. So, we don't
# need to advance to any frame.
clip . requested_frame = clip . frame
else :
# We need to do as many things as we need to get to the next frame.
clip . requested_frame = clip . frame + 1
2021-05-23 22:32:21 +02:00
while True :
2021-05-24 03:24:26 +02:00
# First, see if we need to rewind the clip if we were requested to go backwards
# during some bytecode update in this loop.
if clip . frame > clip . requested_frame :
# Rewind this clip to the beginning so we can replay until the requested frame.
2021-05-25 04:01:36 +02:00
if clip is self . __root :
print ( " WARNING: Root clip was rewound, its possible this animation plays forever! " )
2021-05-24 20:35:43 +02:00
clip . rewind ( )
2021-05-23 22:32:21 +02:00
2021-05-24 03:24:26 +02:00
self . vprint ( f " { prefix } Processing frame { clip . frame } on our way to frame { clip . requested_frame } " )
2021-05-23 22:32:21 +02:00
# 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 ) ]
2021-05-24 03:24:26 +02:00
# Execute each tag in the frame if we need to move forward to a new frame.
if clip . frame != clip . requested_frame :
2021-05-23 22:32:21 +02:00
frame = clip . source . frames [ clip . frame ]
2021-05-24 20:35:43 +02:00
orphans : List [ Tag ] = [ ]
played_tags : Set [ int ] = set ( )
# See if we have any orphans that need to be placed before this frame will work.
for unplayed_tag in clip . unplayed_tags :
if unplayed_tag < frame . start_tag_offset :
self . vprint ( f " { prefix } Including orphaned tag { unplayed_tag } in frame evaluation " )
played_tags . add ( unplayed_tag )
orphans . append ( clip . source . tags [ unplayed_tag ] )
for tagno in range ( frame . start_tag_offset , frame . start_tag_offset + frame . num_tags ) :
played_tags . add ( tagno )
# Check these off our future todo list.
clip . unplayed_tags = [ t for t in clip . unplayed_tags if t not in played_tags ]
2021-05-23 22:32:21 +02:00
2021-05-24 20:35:43 +02:00
# Grab the normal list of tags, add to the orphans.
tags = orphans + clip . source . tags [ frame . start_tag_offset : ( frame . start_tag_offset + frame . num_tags ) ]
2021-05-23 22:32:21 +02:00
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 :
2021-05-24 03:24:26 +02:00
# These are never dirty-only updates as they're fresh-placed.
changed = self . __process_tags ( new_clip , False , prefix = prefix + " " ) or changed
# Now, advance the frame for this clip since we processed the frame.
clip . advance ( )
2021-05-23 22:32:21 +02:00
# Now, handle each of the existing clips.
for child in child_clips :
2021-05-24 03:24:26 +02:00
changed = self . __process_tags ( child , only_dirty , prefix = prefix + " " ) or changed
2021-05-23 22:32:21 +02:00
2021-05-24 03:24:26 +02:00
# See if we're done with this clip.
if clip . frame == clip . requested_frame :
clip . requested_frame = None
2021-05-23 22:32:21 +02:00
break
2021-05-11 00:26:26 +02:00
2021-05-24 03:24:26 +02:00
self . vprint ( f " { prefix } Finished handling { ' dirty updates on ' if only_dirty else ' ' } placed clip { clip . object_id } at depth { clip . depth } " )
2021-05-11 00:26:26 +02:00
# Return if anything was modified.
return changed
2021-05-25 04:01:17 +02:00
def __handle_imports ( self , swf : SWF ) - > Dict [ int , Union [ RegisteredShape , RegisteredClip , RegisteredImage , RegisteredDummy ] ] :
external_objects : Dict [ int , Union [ RegisteredShape , RegisteredClip , RegisteredImage , RegisteredDummy ] ] = { }
2021-05-21 18:58:01 +02:00
# Go through, recursively resolve imports for all SWF files.
for tag_id , imp in swf . imported_tags . items ( ) :
2021-07-07 00:29:41 +02:00
for _name , other in self . swfs . items ( ) :
2021-05-21 18:58:01 +02:00
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 :
2021-05-24 19:38:56 +02:00
# Only display a warning if we don't have our own stub implementation of this SWF.
if repr ( imp ) not in self . __stubbed_swfs :
print ( f " WARNING: { swf . exported_name } imports { imp } but that SWF is not in our library! " )
2021-05-21 18:58:01 +02:00
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
2021-05-25 04:01:17 +02:00
def __find_import ( self , swf : SWF , tag_id : int ) - > Union [ RegisteredShape , RegisteredClip , RegisteredImage , RegisteredDummy ] :
2021-05-21 18:58:01 +02:00
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 ,
2021-05-25 04:01:17 +02:00
swf . labels ,
2021-05-21 18:58:01 +02:00
)
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
2021-05-25 04:01:17 +02:00
def __find_tag ( self , clip : RegisteredClip , tag_id : int ) - > Optional [ Union [ RegisteredShape , RegisteredClip , RegisteredImage , RegisteredDummy ] ] :
2021-05-21 18:58:01 +02:00
# 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 ) ]
2021-07-07 00:29:41 +02:00
for tag in tags :
2021-05-21 18:58:01 +02:00
# 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 ,
)
2021-05-25 04:01:17 +02:00
elif isinstance ( tag , AP2ImageTag ) :
if tag . id == tag_id :
# We need to be able to see this shape to place it.
if tag . reference not in self . textures :
raise Exception ( f " Cannot find texture reference { tag . reference } ! " )
# This matched, so this is the import.
return RegisteredImage (
tag . id ,
tag . reference ,
)
2021-05-21 18:58:01 +02:00
elif isinstance ( tag , AP2DefineSpriteTag ) :
2021-05-25 04:01:17 +02:00
new_clip = RegisteredClip ( tag . id , tag . frames , tag . tags , tag . labels )
2021-05-21 18:58:01 +02:00
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-24 01:37:05 +02:00
def __render (
self ,
swf : SWF ,
only_depths : Optional [ List [ int ] ] ,
2021-05-25 04:01:36 +02:00
only_frames : Optional [ List [ int ] ] ,
2021-05-24 01:37:05 +02:00
movie_transform : Matrix ,
background_image : Optional [ Image . Image ] ,
2021-05-24 19:36:34 +02:00
) - > Generator [ Image . Image , None , None ] :
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-05-24 19:36:34 +02:00
last_rendered_frame : Optional [ Image . Image ] = None
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.
2021-07-06 23:58:32 +02:00
resized_width , resized_height , _ = movie_transform . multiply_point ( Point ( swf . location . width , swf . location . height ) ) . as_tuple ( )
2021-05-21 23:31:13 +02:00
2021-05-29 05:41:25 +02:00
# TODO: If the location top/left is nonzero, we need move the root transform
# so that the correct viewport is rendered.
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-25 04:01:17 +02:00
swf . labels ,
2021-05-11 00:26:26 +02:00
) ,
)
2021-05-24 19:36:57 +02:00
self . __root = root_clip
2021-04-16 01:18:33 +02:00
2021-05-24 01:37:05 +02:00
# If we have a background image, add it to the root clip.
if background_image :
# Stretch the image to make sure it fits the entire frame.
imgwidth = float ( background_image . width )
imgheight = float ( background_image . height )
2021-07-06 23:58:32 +02:00
background_matrix = Matrix . affine (
2021-05-24 01:37:05 +02:00
a = swf . location . width / imgwidth ,
b = 0 ,
c = 0 ,
d = swf . location . height / imgheight ,
tx = 0 ,
ty = 0 ,
)
# Register the background image with the texture library.
name = f " { swf . exported_name } _inserted_background "
self . textures [ name ] = background_image . convert ( " RGBA " )
# Place an instance of this background on the root clip.
root_clip . placed_objects . append (
PlacedShape (
- 1 ,
- 1 ,
Point . identity ( ) ,
background_matrix ,
Color ( 1.0 , 1.0 , 1.0 , 1.0 ) ,
Color ( 0.0 , 0.0 , 0.0 , 0.0 ) ,
0 ,
None ,
RegisteredShape (
- 1 ,
# The coordinates of the rectangle of the shape in screen space.
[
2021-07-06 23:58:32 +02:00
Point ( 0 , 0 ) ,
Point ( imgwidth , 0 ) ,
2021-05-24 01:37:05 +02:00
Point ( imgwidth , imgheight ) ,
2021-07-06 23:58:32 +02:00
Point ( 0 , imgheight ) ,
2021-05-24 01:37:05 +02:00
] ,
# The coordinates of the original texture in UV space (we don't use this).
[
Point ( 0.0 , 0.0 ) ,
Point ( 1.0 , 0.0 ) ,
Point ( 1.0 , 1.0 ) ,
Point ( 0.0 , 1.0 ) ,
] ,
# No texture colors.
[ ] ,
[
DrawParams (
# Instantiable, includes texture.
0x3 ,
# The texture this should use for drawing.
name ,
# The coordinates of the triangles that get drawn (we don't use this).
[ 0 , 1 , 2 , 2 , 1 , 3 ] ,
# The blend color.
None ,
) ,
] ,
) ,
) ,
)
2021-05-23 22:37:18 +02:00
# Create the root mask for where to draw the root clip.
2021-07-06 23:58:32 +02:00
movie_mask = Image . new ( " RGBA " , ( resized_width , resized_height ) , color = ( 255 , 0 , 0 , 255 ) )
2021-05-23 22:37:18 +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-31 20:13:43 +02:00
max_frame : Optional [ int ] = None
2021-05-25 04:01:36 +02:00
if only_frames :
max_frame = max ( only_frames )
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-23 22:33:05 +02:00
while root_clip . playing and not root_clip . finished :
2021-04-21 02:01:35 +02:00
# Create a new image to render into.
2021-05-24 19:36:34 +02:00
self . vprint ( f " Rendering frame { frameno + 1 } / { len ( root_clip . source . frames ) } " )
2021-04-21 02:01:35 +02:00
# Go through all registered clips, place all needed tags.
2021-05-24 03:24:26 +02:00
changed = self . __process_tags ( root_clip , False )
while self . __is_dirty ( root_clip ) :
changed = self . __process_tags ( root_clip , True ) or changed
2021-04-21 02:01:35 +02:00
2021-05-25 04:01:36 +02:00
# If we're only rendering some frames, don't bother to do the draw operations
# if we aren't going to return the frame.
if only_frames and ( frameno + 1 ) not in only_frames :
self . vprint ( f " Skipped rendering frame { frameno + 1 } / { len ( root_clip . source . frames ) } " )
last_rendered_frame = None
frameno + = 1
continue
2021-05-24 19:36:34 +02:00
if changed or last_rendered_frame is None :
2021-05-22 03:30:28 +02:00
# Now, render out the placed objects.
2021-05-25 04:01:36 +02:00
color = swf . color or Color ( 0.0 , 0.0 , 0.0 , 0.0 )
2021-07-06 23:58:32 +02:00
curimage = Image . new ( " RGBA " , ( resized_width , resized_height ) , color = color . as_tuple ( ) )
2021-05-23 22:37:18 +02:00
curimage = self . __render_object ( curimage , root_clip , movie_transform , movie_mask , 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 " )
2021-05-24 19:36:34 +02:00
curimage = last_rendered_frame . copy ( )
2021-04-16 01:18:33 +02:00
2021-05-24 19:36:34 +02:00
# Return that frame, advance our bookkeeping.
2021-05-25 04:01:36 +02:00
self . vprint ( f " Finished rendering frame { frameno + 1 } / { len ( root_clip . source . frames ) } " )
2021-05-24 19:36:34 +02:00
last_rendered_frame = curimage
2021-04-21 02:01:35 +02:00
frameno + = 1
2021-05-24 19:36:34 +02:00
yield curimage
2021-05-25 04:01:36 +02:00
# See if we should bail because we passed the last requested frame.
if max_frame is not None and frameno == max_frame :
break
2021-04-21 02:01:35 +02:00
except KeyboardInterrupt :
# Allow ctrl-c to end early and render a partial animation.
2021-05-24 19:36:34 +02:00
print ( f " WARNING: Interrupted early, will render only { frameno } / { 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
2021-05-25 04:01:36 +02:00
self . __root = None