729 lines
33 KiB
Python
729 lines
33 KiB
Python
from typing import Dict, List, Tuple, Optional, Union
|
|
from PIL import Image # type: ignore
|
|
|
|
from .blend import affine_composite
|
|
from .swf import SWF, Frame, Tag, AP2ShapeTag, AP2DefineSpriteTag, AP2PlaceObjectTag, AP2RemoveObjectTag, AP2DoActionTag, AP2DefineFontTag, AP2DefineEditTextTag, AP2PlaceCameraTag
|
|
from .types import Color, Matrix, Point, Rectangle
|
|
from .geo import Shape, DrawParams
|
|
from .util import VerboseOutput
|
|
|
|
|
|
class RegisteredClip:
|
|
# A movie clip that we are rendering, frame by frame. These are manifest by the root
|
|
# 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:
|
|
self.tag_id = tag_id
|
|
self.frames = frames
|
|
self.tags = tags
|
|
|
|
def __repr__(self) -> str:
|
|
return f"RegisteredClip(tag_id={self.tag_id})"
|
|
|
|
|
|
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
|
|
self.rectangle: Optional[Image.image] = None
|
|
|
|
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})"
|
|
|
|
|
|
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})"
|
|
|
|
|
|
class PlacedObject:
|
|
# An object that occupies the screen at some depth.
|
|
def __init__(self, object_id: int, depth: int, rotation_offset: Point, transform: Matrix, mult_color: Color, add_color: Color, blend: int) -> None:
|
|
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
|
|
|
|
@property
|
|
def source(self) -> Union[RegisteredClip, RegisteredShape, RegisteredDummy]:
|
|
raise NotImplementedError("Only implemented in subclass!")
|
|
|
|
@property
|
|
def depth(self) -> int:
|
|
return self.__depth
|
|
|
|
@property
|
|
def object_id(self) -> int:
|
|
return self.__object_id
|
|
|
|
def __repr__(self) -> str:
|
|
return f"PlacedObject(object_id={self.object_id}, depth={self.depth})"
|
|
|
|
|
|
class PlacedShape(PlacedObject):
|
|
# A shape that occupies its parent clip at some depth. Placed by an AP2PlaceObjectTag
|
|
# referencing an AP2ShapeTag.
|
|
def __init__(self, object_id: int, depth: int, rotation_offset: Point, transform: Matrix, mult_color: Color, add_color: Color, blend: int, source: RegisteredShape) -> None:
|
|
super().__init__(object_id, depth, rotation_offset, transform, mult_color, add_color, blend)
|
|
self.__source = source
|
|
|
|
@property
|
|
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.
|
|
def __init__(self, object_id: int, depth: int, rotation_offset: Point, transform: Matrix, mult_color: Color, add_color: Color, blend: int, source: RegisteredClip) -> None:
|
|
super().__init__(object_id, depth, rotation_offset, transform, mult_color, add_color, blend)
|
|
self.placed_objects: List[PlacedObject] = []
|
|
self.frame: int = 0
|
|
self.__source = source
|
|
|
|
@property
|
|
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)
|
|
|
|
def __repr__(self) -> str:
|
|
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})"
|
|
|
|
|
|
class PlacedDummy(PlacedObject):
|
|
# A reference to an object we can't find because we're missing the import.
|
|
def __init__(self, object_id: int, depth: int, rotation_offset: Point, transform: Matrix, mult_color: Color, add_color: Color, blend: int, source: RegisteredDummy) -> None:
|
|
super().__init__(object_id, depth, rotation_offset, transform, mult_color, add_color, blend)
|
|
self.__source = source
|
|
|
|
@property
|
|
def source(self) -> RegisteredDummy:
|
|
return self.__source
|
|
|
|
|
|
class AFPRenderer(VerboseOutput):
|
|
def __init__(self, shapes: Dict[str, Shape] = {}, textures: Dict[str, Image.Image] = {}, swfs: Dict[str, SWF] = {}, single_threaded: bool = False) -> None:
|
|
super().__init__()
|
|
|
|
# Options for rendering
|
|
self.__single_threaded = single_threaded
|
|
|
|
# Library of shapes (draw instructions), textures (actual images) and swfs (us and other files for imports).
|
|
self.shapes: Dict[str, Shape] = shapes
|
|
self.textures: Dict[str, Image.Image] = textures
|
|
self.swfs: Dict[str, SWF] = swfs
|
|
|
|
# Internal render parameters.
|
|
self.__registered_objects: Dict[int, Union[RegisteredShape, RegisteredClip, RegisteredDummy]] = {}
|
|
|
|
def add_shape(self, name: str, data: Shape) -> None:
|
|
# Register a named shape with the renderer.
|
|
if not data.parsed:
|
|
data.parse()
|
|
self.shapes[name] = data
|
|
|
|
def add_texture(self, name: str, data: Image.Image) -> None:
|
|
# Register a named texture (already loaded PIL image) with the renderer.
|
|
self.textures[name] = data.convert("RGBA")
|
|
|
|
def add_swf(self, name: str, data: SWF) -> None:
|
|
# Register a named SWF with the renderer.
|
|
if not data.parsed:
|
|
data.parse()
|
|
self.swfs[name] = data
|
|
|
|
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]]:
|
|
# Given a path to a SWF root animation, attempt to render it to a list of frames.
|
|
for name, swf in self.swfs.items():
|
|
if swf.exported_name == path:
|
|
# This is the SWF we care about.
|
|
with self.debugging(verbose):
|
|
swf.color = background_color or swf.color
|
|
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
|
|
|
|
raise Exception(f'{path} not found in registered SWFs!')
|
|
|
|
def list_paths(self, verbose: bool = False) -> List[str]:
|
|
# Given the loaded animations, return a list of possible paths to render.
|
|
paths: List[str] = []
|
|
|
|
for name, swf in self.swfs.items():
|
|
paths.append(swf.exported_name)
|
|
|
|
return paths
|
|
|
|
def __place(self, tag: Tag, operating_clip: PlacedClip, prefix: str = "") -> Tuple[Optional[PlacedClip], bool]:
|
|
# "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.
|
|
if isinstance(tag, AP2ShapeTag):
|
|
self.vprint(f"{prefix} Loading {tag.reference} into object slot {tag.id}")
|
|
|
|
if tag.reference not in self.shapes:
|
|
raise Exception(f"Cannot find shape reference {tag.reference}!")
|
|
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,
|
|
)
|
|
|
|
# Didn't place a new clip, didn't change anything.
|
|
return None, False
|
|
|
|
elif isinstance(tag, AP2DefineSpriteTag):
|
|
self.vprint(f"{prefix} Loading Sprite into object slot {tag.id}")
|
|
|
|
if tag.id in self.__registered_objects:
|
|
raise Exception(f"Cannot register sprite as object slot {tag.id} is already taken!")
|
|
|
|
# Register a new clip that we might reference to execute.
|
|
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
|
|
|
|
elif isinstance(tag, AP2PlaceObjectTag):
|
|
if tag.update:
|
|
for i in range(len(operating_clip.placed_objects) - 1, -1, -1):
|
|
obj = operating_clip.placed_objects[i]
|
|
|
|
if obj.object_id == tag.object_id and obj.depth == tag.depth:
|
|
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,
|
|
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,
|
|
newobj,
|
|
)
|
|
operating_clip.placed_objects[i] = new_clip
|
|
|
|
# Placed a new clip, changed the parent.
|
|
return new_clip, True
|
|
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,
|
|
newobj,
|
|
)
|
|
|
|
# Didn't place a new clip, changed the parent clip.
|
|
return None, True
|
|
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
|
|
|
|
# Didn't place a new clip, did change something.
|
|
print(f"WARNING: Couldn't find tag {tag.object_id} on depth {tag.depth} to update!")
|
|
return None, False
|
|
else:
|
|
if tag.source_tag_id is None:
|
|
raise Exception("Cannot place a tag with no source ID and no update flags!")
|
|
|
|
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,
|
|
newobj,
|
|
)
|
|
)
|
|
|
|
# Didn't place a new clip, changed the parent clip.
|
|
return None, True
|
|
elif isinstance(newobj, RegisteredClip):
|
|
# TODO: Handle ON_LOAD triggers for this object. Many of these are just calls into
|
|
# the game to set the current frame that we're on, but sometimes its important.
|
|
for flags, code in tag.triggers.items():
|
|
for bytecode in code:
|
|
print("WARNING: Unhandled PLACE_OBJECT trigger!")
|
|
if self.verbose:
|
|
print(bytecode.decompile())
|
|
|
|
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,
|
|
newobj,
|
|
)
|
|
operating_clip.placed_objects.append(placed_clip)
|
|
|
|
# Placed a new clip, changed the parent.
|
|
return placed_clip, True
|
|
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,
|
|
newobj,
|
|
)
|
|
)
|
|
|
|
# Didn't place a new clip, changed the parent clip.
|
|
return None, True
|
|
else:
|
|
raise Exception(f"Unrecognized object with Tag ID {tag.source_tag_id}!")
|
|
|
|
raise Exception(f"Cannot find a shape or sprite with Tag ID {tag.source_tag_id}!")
|
|
|
|
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.
|
|
# Remember removed objects so we can stop any clips.
|
|
removed_objects = [
|
|
obj for obj in operating_clip.placed_objects
|
|
if obj.object_id == tag.object_id and obj.depth == tag.depth
|
|
]
|
|
|
|
# Get rid of the objects that we're removing from the master list.
|
|
operating_clip.placed_objects = [
|
|
obj for obj in operating_clip.placed_objects
|
|
if not(obj.object_id == tag.object_id and obj.depth == tag.depth)
|
|
]
|
|
else:
|
|
# 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.
|
|
removed_objects = []
|
|
for i in range(len(operating_clip.placed_objects)):
|
|
real_index = len(operating_clip.placed_objects) - (i + 1)
|
|
|
|
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):]
|
|
break
|
|
|
|
if not removed_objects:
|
|
print(f"WARNING: Couldn't find object to remove by ID {tag.object_id} and depth {tag.depth}!")
|
|
|
|
# Didn't place a new clip, changed parent clip.
|
|
return None, True
|
|
|
|
elif isinstance(tag, AP2DoActionTag):
|
|
print("WARNING: Unhandled DO_ACTION tag!")
|
|
if self.verbose:
|
|
print(tag.bytecode.decompile())
|
|
|
|
# Didn't place a new clip.
|
|
return None, False
|
|
|
|
elif isinstance(tag, AP2DefineFontTag):
|
|
print("WARNING: Unhandled DEFINE_FONT tag!")
|
|
|
|
# Didn't place a new clip.
|
|
return None, False
|
|
|
|
elif isinstance(tag, AP2DefineEditTextTag):
|
|
print("WARNING: Unhandled DEFINE_EDIT_TEXT tag!")
|
|
|
|
# Didn't place a new clip.
|
|
return None, False
|
|
|
|
elif isinstance(tag, AP2PlaceCameraTag):
|
|
print("WARNING: Unhandled PLACE_CAMERA tag!")
|
|
|
|
# Didn't place a new clip.
|
|
return None, False
|
|
|
|
else:
|
|
raise Exception(f"Failed to process tag: {tag}")
|
|
|
|
def __render_object(
|
|
self,
|
|
img: Image.Image,
|
|
renderable: PlacedObject,
|
|
parent_transform: Matrix,
|
|
parent_mult_color: Color,
|
|
parent_add_color: Color,
|
|
only_depths: Optional[List[int]] = None,
|
|
prefix: str="",
|
|
) -> Image.Image:
|
|
self.vprint(f"{prefix} Rendering placed object ID {renderable.object_id} from sprite {renderable.source.tag_id} onto Depth {renderable.depth}")
|
|
|
|
# Compute the affine transformation matrix for this object.
|
|
transform = renderable.transform.multiply(parent_transform).translate(Point.identity().subtract(renderable.rotation_offset))
|
|
|
|
# Calculate blending and blend color if it is present.
|
|
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)
|
|
blend = renderable.blend or 0
|
|
|
|
# Render individual shapes if this is a sprite.
|
|
if isinstance(renderable, PlacedClip):
|
|
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
|
|
|
|
if blend not in {0, 2}:
|
|
print(f"WARNING: Unsupported sprite blend {blend}!")
|
|
|
|
# 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
|
|
img = self.__render_object(img, obj, transform, mult_color, add_color, only_depths=new_only_depths, prefix=prefix + " ")
|
|
elif isinstance(renderable, PlacedShape):
|
|
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.
|
|
shape = renderable.source
|
|
|
|
# Now, render out shapes.
|
|
for params in shape.draw_params:
|
|
if not (params.flags & 0x1):
|
|
# Not instantiable, don't render.
|
|
return img
|
|
|
|
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]
|
|
|
|
if params.flags & 0x8:
|
|
# TODO: This texture gets further blended somehow? Not sure this is ever used.
|
|
print(f"WARNING: Unhandled texture blend color {params.blend}!")
|
|
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.
|
|
if len(shape.vertex_points) != 4:
|
|
print("WARNING: Unsupported non-rectangle shape!")
|
|
|
|
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)
|
|
|
|
# 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!")
|
|
|
|
shape.rectangle = Image.new('RGBA', (int(right - left), int(bottom - top)), (params.blend.as_tuple()))
|
|
texture = shape.rectangle
|
|
|
|
if texture is not None:
|
|
img = affine_composite(img, add_color, mult_color, transform, blend, texture, single_threaded=self.__single_threaded)
|
|
elif isinstance(renderable, PlacedDummy):
|
|
# Nothing to do!
|
|
pass
|
|
else:
|
|
raise Exception(f"Unknown placed object type to render {renderable}!")
|
|
|
|
return img
|
|
|
|
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
|
|
|
|
# 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()
|
|
|
|
self.vprint(f"{prefix}Finished handling placed clip {clip.object_id} at depth {clip.depth}")
|
|
|
|
# Return if anything was modified.
|
|
return changed
|
|
|
|
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
|
|
|
|
def __render(self, swf: SWF, only_depths: Optional[List[int]], movie_transform: Matrix) -> Tuple[int, List[Image.Image]]:
|
|
# First, let's attempt to resolve imports.
|
|
self.__registered_objects = self.__handle_imports(swf)
|
|
|
|
# Initialize overall frame advancement stuff.
|
|
spf = 1.0 / swf.fps
|
|
frames: List[Image.Image] = []
|
|
frameno: int = 0
|
|
|
|
# Calculate actual size based on given movie transform.
|
|
actual_size = movie_transform.multiply_point(Point(swf.location.width, swf.location.height)).as_tuple()
|
|
|
|
# Create a root clip for the movie to play.
|
|
root_clip = PlacedClip(
|
|
-1,
|
|
-1,
|
|
Point.identity(),
|
|
Matrix.identity(),
|
|
Color(1.0, 1.0, 1.0, 1.0),
|
|
Color(0.0, 0.0, 0.0, 0.0),
|
|
0,
|
|
RegisteredClip(
|
|
None,
|
|
swf.frames,
|
|
swf.tags,
|
|
),
|
|
)
|
|
|
|
# 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)
|
|
|
|
# Now play the frames of the root clip.
|
|
try:
|
|
while not root_clip.finished:
|
|
# Create a new image to render into.
|
|
time = spf * frameno
|
|
color = swf.color or Color(0.0, 0.0, 0.0, 0.0)
|
|
self.vprint(f"Rendering frame {frameno}/{len(root_clip.source.frames)} ({round(time, 2)}s)")
|
|
|
|
# Go through all registered clips, place all needed tags.
|
|
changed = self.__process_tags(root_clip)
|
|
|
|
if changed or frameno == 0:
|
|
# Now, render out the placed objects.
|
|
curimage = Image.new("RGBA", actual_size, color=color.as_tuple())
|
|
curimage = self.__render_object(curimage, root_clip, movie_transform, actual_mult_color, actual_add_color, only_depths=only_depths)
|
|
else:
|
|
# Nothing changed, make a copy of the previous render.
|
|
self.vprint(" Using previous frame render")
|
|
curimage = frames[-1].copy()
|
|
|
|
# Advance our bookkeeping.
|
|
frames.append(curimage)
|
|
frameno += 1
|
|
except KeyboardInterrupt:
|
|
# Allow ctrl-c to end early and render a partial animation.
|
|
print(f"WARNING: Interrupted early, will render only {len(frames)}/{len(root_clip.source.frames)} frames of animation!")
|
|
|
|
return int(spf * 1000.0), frames
|