2354 lines
90 KiB
Python
2354 lines
90 KiB
Python
from typing import Any, Dict, Generator, List, Set, Tuple, Optional, Union
|
|
from PIL import Image # type: ignore
|
|
|
|
from .blend import affine_composite, perspective_composite
|
|
from .swf import (
|
|
SWF,
|
|
Frame,
|
|
Tag,
|
|
AP2ShapeTag,
|
|
AP2DefineSpriteTag,
|
|
AP2PlaceObjectTag,
|
|
AP2RemoveObjectTag,
|
|
AP2DoActionTag,
|
|
AP2DefineFontTag,
|
|
AP2DefineEditTextTag,
|
|
AP2DefineMorphShapeTag,
|
|
AP2PlaceCameraTag,
|
|
AP2ImageTag,
|
|
)
|
|
from .decompile import ByteCode
|
|
from .types import (
|
|
Color,
|
|
HSL,
|
|
Matrix,
|
|
Point,
|
|
Rectangle,
|
|
AAMode,
|
|
AP2Trigger,
|
|
AP2Action,
|
|
PushAction,
|
|
StoreRegisterAction,
|
|
StringConstant,
|
|
Register,
|
|
NULL,
|
|
UNDEFINED,
|
|
GLOBAL,
|
|
ROOT,
|
|
PARENT,
|
|
THIS,
|
|
CLIP,
|
|
)
|
|
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],
|
|
labels: Dict[str, int],
|
|
) -> None:
|
|
self.tag_id = tag_id
|
|
self.frames = frames
|
|
self.tags = tags
|
|
self.labels = labels
|
|
|
|
def __repr__(self) -> str:
|
|
return f"RegisteredClip(tag_id={self.tag_id})"
|
|
|
|
@property
|
|
def reference(self) -> str:
|
|
return "anonymous sprite"
|
|
|
|
|
|
class RegisteredShape:
|
|
# A shape that we are rendering, as placed by some placed clip somewhere.
|
|
def __init__(
|
|
self,
|
|
tag_id: int,
|
|
reference: str,
|
|
vertex_points: List[Point],
|
|
tex_points: List[Point],
|
|
tex_colors: List[Color],
|
|
draw_params: List[DrawParams],
|
|
) -> None:
|
|
self.tag_id = tag_id
|
|
self.__reference = reference
|
|
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
|
|
|
|
@property
|
|
def reference(self) -> str:
|
|
textures = {dp.region for dp in self.draw_params if dp.region is not None}
|
|
if textures:
|
|
vals = ", ".join(textures)
|
|
return f"{self.__reference}, {vals}"
|
|
else:
|
|
return f"{self.__reference}, untextured"
|
|
|
|
def __repr__(self) -> str:
|
|
return f"RegisteredShape(tag_id={self.tag_id}, reference={self.reference} vertex_points={self.vertex_points}, tex_points={self.tex_points}, tex_colors={self.tex_colors}, draw_params={self.draw_params})"
|
|
|
|
|
|
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})"
|
|
|
|
|
|
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})"
|
|
|
|
@property
|
|
def reference(self) -> str:
|
|
return "anonymous dummy"
|
|
|
|
|
|
class Mask:
|
|
def __init__(self, bounds: Rectangle) -> None:
|
|
self.bounds = bounds
|
|
self.rectangle: Optional[Image.Image] = None
|
|
|
|
|
|
class PlacedObject:
|
|
# An object that occupies the screen at some depth.
|
|
def __init__(
|
|
self,
|
|
object_id: int,
|
|
depth: int,
|
|
rotation_origin: Point,
|
|
transform: Matrix,
|
|
projection: int,
|
|
mult_color: Color,
|
|
add_color: Color,
|
|
hsl_shift: HSL,
|
|
blend: int,
|
|
mask: Optional[Mask],
|
|
) -> None:
|
|
self.__object_id = object_id
|
|
self.__depth = depth
|
|
self.rotation_origin = rotation_origin
|
|
self.transform = transform
|
|
self.projection = projection
|
|
self.mult_color = mult_color
|
|
self.add_color = add_color
|
|
self.hsl_shift = hsl_shift
|
|
self.blend = blend
|
|
self.mask = mask
|
|
self.visible: bool = True
|
|
|
|
@property
|
|
def source(
|
|
self,
|
|
) -> Union[RegisteredClip, RegisteredShape, RegisteredImage, 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_origin: Point,
|
|
transform: Matrix,
|
|
projection: int,
|
|
mult_color: Color,
|
|
add_color: Color,
|
|
hsl_shift: HSL,
|
|
blend: int,
|
|
mask: Optional[Mask],
|
|
source: RegisteredShape,
|
|
) -> None:
|
|
super().__init__(
|
|
object_id,
|
|
depth,
|
|
rotation_origin,
|
|
transform,
|
|
projection,
|
|
mult_color,
|
|
add_color,
|
|
hsl_shift,
|
|
blend,
|
|
mask,
|
|
)
|
|
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_origin: Point,
|
|
transform: Matrix,
|
|
projection: int,
|
|
mult_color: Color,
|
|
add_color: Color,
|
|
hsl_shift: HSL,
|
|
blend: int,
|
|
mask: Optional[Mask],
|
|
source: RegisteredClip,
|
|
) -> None:
|
|
super().__init__(
|
|
object_id,
|
|
depth,
|
|
rotation_origin,
|
|
transform,
|
|
projection,
|
|
mult_color,
|
|
add_color,
|
|
hsl_shift,
|
|
blend,
|
|
mask,
|
|
)
|
|
self.placed_objects: List[PlacedObject] = []
|
|
self.frame: int = 0
|
|
self.unplayed_tags: List[int] = [i for i in range(len(source.tags))]
|
|
self.__source = source
|
|
|
|
# Dynamic properties that are adjustable by SWF bytecode.
|
|
self.playing: bool = True
|
|
self.requested_frame: Optional[int] = None
|
|
self.visible_frame: int = -1
|
|
|
|
# Root clip resizing, which we don't really support.
|
|
self.__width = 0
|
|
self.__height = 0
|
|
|
|
@property
|
|
def source(self) -> RegisteredClip:
|
|
return self.__source
|
|
|
|
def __check_visible(self) -> None:
|
|
if self.visible_frame >= 0 and self.frame >= self.visible_frame:
|
|
self.visible = True
|
|
self.visible_frame = -1
|
|
|
|
def advance(self) -> None:
|
|
if self.frame < len(self.source.frames):
|
|
self.frame += 1
|
|
self.__check_visible()
|
|
|
|
def rewind(self) -> None:
|
|
self.frame = 0
|
|
self.unplayed_tags = [i for i in range(len(self.__source.tags))]
|
|
self.placed_objects = []
|
|
self.__check_visible()
|
|
|
|
@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}, "
|
|
+ f"requested_frame={self.requested_frame}, total_frames={len(self.source.frames)}, playing={self.playing}, "
|
|
+ f"finished={self.finished})"
|
|
)
|
|
|
|
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
|
|
|
|
# The following are attributes and functions necessary to support some simple bytecode.
|
|
def gotoAndStop(self, frame: Any) -> None:
|
|
actual_frame = self.__resolve_frame(frame)
|
|
if actual_frame is None:
|
|
print(f"WARNING: Unrecognized frame {frame} to gotoAndStop function!")
|
|
return
|
|
if actual_frame <= 0:
|
|
actual_frame = 1
|
|
if actual_frame > len(self.source.frames):
|
|
actual_frame = len(self.source.frames)
|
|
self.requested_frame = actual_frame
|
|
self.playing = False
|
|
|
|
def gotoAndPlay(self, frame: Any) -> None:
|
|
actual_frame = self.__resolve_frame(frame)
|
|
if actual_frame is None:
|
|
print(f"WARNING: Non-integer frame {frame} to gotoAndPlay function!")
|
|
return
|
|
if actual_frame <= 0:
|
|
actual_frame = 1
|
|
if actual_frame > len(self.source.frames):
|
|
actual_frame = len(self.source.frames)
|
|
self.requested_frame = actual_frame
|
|
self.playing = True
|
|
|
|
def stop(self) -> None:
|
|
self.playing = False
|
|
|
|
def play(self) -> None:
|
|
self.playing = True
|
|
|
|
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
|
|
actual_frame += self.frameOffset - 1
|
|
self.visible = False
|
|
if actual_frame <= 0:
|
|
actual_frame = 1
|
|
if actual_frame > len(self.source.frames):
|
|
actual_frame = len(self.source.frames)
|
|
self.visible_frame = actual_frame
|
|
self.__check_visible()
|
|
|
|
@property
|
|
def frameOffset(self) -> int:
|
|
return self.requested_frame or self.frame
|
|
|
|
@frameOffset.setter
|
|
def frameOffset(self, val: Any) -> None:
|
|
actual_frame = self.__resolve_frame(val)
|
|
if actual_frame is None:
|
|
print(f"WARNING: Non-integer frameOffset {val} to frameOffset attribute!")
|
|
return
|
|
if actual_frame < 0:
|
|
actual_frame = 0
|
|
if actual_frame >= len(self.source.frames):
|
|
actual_frame = len(self.source.frames) - 1
|
|
self.requested_frame = actual_frame + 1
|
|
|
|
@property
|
|
def _visible(self) -> int:
|
|
return 1 if self.visible else 0
|
|
|
|
@_visible.setter
|
|
def _visible(self, val: Any) -> None:
|
|
self.visible = val != 0
|
|
|
|
@property
|
|
def _width(self) -> int:
|
|
calculated_width = self.__width
|
|
for obj in self.placed_objects:
|
|
if isinstance(obj, PlacedClip):
|
|
calculated_width = max(calculated_width, obj._width)
|
|
return calculated_width
|
|
|
|
@_width.setter
|
|
def _width(self, val: Any) -> None:
|
|
self.__width = val
|
|
|
|
@property
|
|
def _height(self) -> int:
|
|
calculated_height = self.__height
|
|
for obj in self.placed_objects:
|
|
if isinstance(obj, PlacedClip):
|
|
calculated_height = max(calculated_height, obj._height)
|
|
return calculated_height
|
|
|
|
@_height.setter
|
|
def _height(self, val: Any) -> None:
|
|
self.__height = val
|
|
|
|
|
|
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,
|
|
rotation_origin: Point,
|
|
transform: Matrix,
|
|
projection: int,
|
|
mult_color: Color,
|
|
add_color: Color,
|
|
hsl_shift: HSL,
|
|
blend: int,
|
|
mask: Optional[Mask],
|
|
source: RegisteredImage,
|
|
) -> None:
|
|
super().__init__(
|
|
object_id,
|
|
depth,
|
|
rotation_origin,
|
|
transform,
|
|
projection,
|
|
mult_color,
|
|
add_color,
|
|
hsl_shift,
|
|
blend,
|
|
mask,
|
|
)
|
|
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})"
|
|
|
|
|
|
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_origin: Point,
|
|
transform: Matrix,
|
|
projection: int,
|
|
mult_color: Color,
|
|
add_color: Color,
|
|
hsl_shift: HSL,
|
|
blend: int,
|
|
mask: Optional[Mask],
|
|
source: RegisteredDummy,
|
|
) -> None:
|
|
super().__init__(
|
|
object_id,
|
|
depth,
|
|
rotation_origin,
|
|
transform,
|
|
projection,
|
|
mult_color,
|
|
add_color,
|
|
hsl_shift,
|
|
blend,
|
|
mask,
|
|
)
|
|
self.__source = source
|
|
|
|
@property
|
|
def source(self) -> RegisteredDummy:
|
|
return self.__source
|
|
|
|
|
|
class PlacedCamera:
|
|
def __init__(self, center: Point, focal_length: float) -> None:
|
|
self.center = center
|
|
self.focal_length = focal_length
|
|
self.adjusted = False
|
|
|
|
|
|
class Global:
|
|
def __init__(self, root: PlacedClip, clip: PlacedClip) -> None:
|
|
self.root = root
|
|
self.clip = clip
|
|
|
|
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.clip.placed_objects:
|
|
if obj.depth == depth:
|
|
return obj
|
|
|
|
print(f"WARNING: Could not find object at depth {depth}!")
|
|
return UNDEFINED
|
|
|
|
def deepGotoAndPlay(self, frame: Any) -> Any:
|
|
# This is identical to regular gotoAndPlay, however it also recursively
|
|
# goes through and sets all child clips playing as well.
|
|
try:
|
|
meth = getattr(self.clip, "gotoAndPlay")
|
|
|
|
# Call it, set the return on the stack.
|
|
retval = meth(frame)
|
|
|
|
# Recursively go through any children of "clip" and call play
|
|
# on them as well.
|
|
def play_children(obj: Any) -> None:
|
|
if isinstance(obj, PlacedClip):
|
|
obj.play()
|
|
for child in obj.placed_objects:
|
|
play_children(child)
|
|
|
|
play_children(self.clip)
|
|
return retval
|
|
except AttributeError:
|
|
# Function does not exist!
|
|
print(
|
|
f"WARNING: Tried to call gotoAndPlay({frame}) on {self.clip} but that method doesn't exist!"
|
|
)
|
|
return UNDEFINED
|
|
|
|
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)
|
|
|
|
|
|
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))
|
|
):
|
|
print(
|
|
f"WARNING: Ignoring aeplib.aep_set_rect_mask call with invalid parameters {left}, {right}, {top}, {bottom}!"
|
|
)
|
|
return
|
|
if isinstance(thisptr, PlacedObject):
|
|
thisptr.mask = Mask(
|
|
Rectangle(
|
|
left=float(left),
|
|
right=float(right),
|
|
top=float(top),
|
|
bottom=float(bottom),
|
|
),
|
|
)
|
|
else:
|
|
print(
|
|
f"WARNING: Ignoring aeplib.aep_set_rect_mask call with unrecognized target {thisptr}!"
|
|
)
|
|
|
|
def aep_set_set_frame(self, thisptr: Any, frame: Any) -> None:
|
|
# 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.
|
|
pass
|
|
|
|
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
|
|
|
|
obj.setInvisibleUntil(frame)
|
|
return
|
|
|
|
print(
|
|
f"WARNING: Ignoring aeplib.aep_set_frame_control called on nonexistent object at depth {depth}!"
|
|
)
|
|
|
|
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!
|
|
print(
|
|
f"WARNING: Tried to call gotoAndPlay({frame}) on {thisptr} but that method doesn't exist!"
|
|
)
|
|
return UNDEFINED
|
|
|
|
def gotoAndStop(self, thisptr: Any, frame: Any) -> Any:
|
|
# This appears to be a wrapper to allow calling gotoAndStop on clips.
|
|
try:
|
|
meth = getattr(thisptr, "gotoAndStop")
|
|
|
|
# Call it, set the return on the stack.
|
|
return meth(frame)
|
|
except AttributeError:
|
|
# Function does not exist!
|
|
print(
|
|
f"WARNING: Tried to call gotoAndStop({frame}) on {thisptr} but that method doesn't exist!"
|
|
)
|
|
return UNDEFINED
|
|
|
|
def deepGotoAndPlay(self, thisptr: Any, frame: Any) -> Any:
|
|
# This is identical to regular gotoAndPlay, however it also recursively
|
|
# goes through and sets all child clips playing as well.
|
|
try:
|
|
meth = getattr(thisptr, "gotoAndPlay")
|
|
|
|
# Call it, set the return on the stack.
|
|
retval = meth(frame)
|
|
|
|
# Recursively go through any children of "thisptr" and call play
|
|
# on them as well.
|
|
def play_children(obj: Any) -> None:
|
|
if isinstance(obj, PlacedClip):
|
|
obj.play()
|
|
for child in obj.placed_objects:
|
|
play_children(child)
|
|
|
|
play_children(thisptr)
|
|
return retval
|
|
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 deepGotoAndStop(self, thisptr: Any, frame: Any) -> Any:
|
|
# This is identical to regular gotoAndStop, however it also recursively
|
|
# goes through and sets all child clips stopped as well.
|
|
try:
|
|
meth = getattr(thisptr, "gotoAndStop")
|
|
|
|
# Call it, set the return on the stack.
|
|
retval = meth(frame)
|
|
|
|
# Recursively go through any children of "thisptr" and call stop
|
|
# on them as well.
|
|
def stop_children(obj: Any) -> None:
|
|
if isinstance(obj, PlacedClip):
|
|
obj.stop()
|
|
for child in obj.placed_objects:
|
|
stop_children(child)
|
|
|
|
stop_children(thisptr)
|
|
return retval
|
|
except AttributeError:
|
|
# Function does not exist!
|
|
print(
|
|
f"WARNING: Tried to call gotoAndStop({frame}) on {thisptr} but that method doesn't exist!"
|
|
)
|
|
return UNDEFINED
|
|
|
|
def play(self, thisptr: Any) -> Any:
|
|
# This appears to be a wrapper to allow calling play on clips.
|
|
try:
|
|
meth = getattr(thisptr, "play")
|
|
|
|
# Call it, set the return on the stack.
|
|
return meth()
|
|
except AttributeError:
|
|
# Function does not exist!
|
|
print(
|
|
f"WARNING: Tried to call play() 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!"
|
|
)
|
|
return UNDEFINED
|
|
|
|
|
|
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!"
|
|
)
|
|
|
|
|
|
MissingThis = object()
|
|
|
|
|
|
class AFPRenderer(VerboseOutput):
|
|
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:
|
|
super().__init__()
|
|
|
|
# Options for rendering
|
|
self.__single_threaded = single_threaded
|
|
self.__enable_aa = enable_aa
|
|
|
|
# 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, RegisteredImage, RegisteredDummy],
|
|
] = {}
|
|
self.__root: Optional[PlacedClip] = None
|
|
self.__camera: Optional[PlacedCamera] = None
|
|
|
|
# List of imports that we provide stub implementations for.
|
|
self.__stubbed_swfs: Set[str] = {
|
|
"aeplib.aeplib",
|
|
"aeplib.__Packages.aeplib",
|
|
}
|
|
|
|
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,
|
|
background_image: Optional[List[Image.Image]] = None,
|
|
only_depths: Optional[List[int]] = None,
|
|
only_frames: Optional[List[int]] = None,
|
|
movie_transform: Matrix = Matrix.identity(),
|
|
overridden_width: Optional[float] = None,
|
|
overridden_height: Optional[float] = None,
|
|
verbose: bool = False,
|
|
) -> Generator[Image.Image, None, None]:
|
|
# 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
|
|
yield from self.__render(
|
|
swf,
|
|
only_depths,
|
|
only_frames,
|
|
movie_transform,
|
|
background_image,
|
|
overridden_width,
|
|
overridden_height,
|
|
)
|
|
return
|
|
|
|
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 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.
|
|
for _name, swf in self.swfs.items():
|
|
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!")
|
|
|
|
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.
|
|
for _name, swf in self.swfs.items():
|
|
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!")
|
|
|
|
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.
|
|
for _name, swf in self.swfs.items():
|
|
if swf.exported_name == path:
|
|
return swf.location
|
|
|
|
raise Exception(f"{path} not found in registered SWFs!")
|
|
|
|
def list_paths(self, verbose: bool = False) -> Generator[str, None, None]:
|
|
# Given the loaded animations, return a list of possible paths to render.
|
|
for _name, swf in self.swfs.items():
|
|
yield swf.exported_name
|
|
|
|
def __execute_bytecode(
|
|
self,
|
|
bytecode: ByteCode,
|
|
clip: PlacedClip,
|
|
thisptr: Optional[Any] = MissingThis,
|
|
prefix: str = "",
|
|
) -> None:
|
|
if self.__root is None:
|
|
raise Exception(
|
|
"Logic error, executing bytecode outside of a rendering movie clip!"
|
|
)
|
|
|
|
thisobj = clip if (thisptr is MissingThis) else thisptr
|
|
globalobj = Global(self.__root, clip)
|
|
location: int = 0
|
|
stack: List[Any] = []
|
|
variables: Dict[str, Any] = {
|
|
"aeplib": AEPLib(),
|
|
"asdlib": ASDLib(),
|
|
}
|
|
registers: List[Any] = [UNDEFINED] * 256
|
|
|
|
self.vprint(f"{prefix}Bytecode engine starting.", component="bytecode")
|
|
|
|
while location < len(bytecode.actions):
|
|
action = bytecode.actions[location]
|
|
|
|
if action.opcode == AP2Action.END:
|
|
# End the execution.
|
|
self.vprint(
|
|
f"{prefix} Ending bytecode execution.", component="bytecode"
|
|
)
|
|
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} to {set_value} but that attribute doesn't exist!"
|
|
)
|
|
else:
|
|
self.vprint(
|
|
f"{prefix} Setting attribute {attribute} on {obj} to {set_value}",
|
|
component="bytecode",
|
|
)
|
|
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:
|
|
self.vprint(
|
|
f"{prefix} Calling method {methname}({', '.join(repr(s) for s in params)}) on {obj}",
|
|
component="bytecode",
|
|
)
|
|
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:
|
|
self.vprint(
|
|
f"{prefix} Calling global function {funcname}({', '.join(repr(s) for s in params)})",
|
|
component="bytecode",
|
|
)
|
|
func = getattr(globalobj, 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 {globalobj} 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))
|
|
elif obj is NULL:
|
|
stack.append(None)
|
|
elif obj is THIS:
|
|
stack.append(thisobj)
|
|
elif obj is GLOBAL:
|
|
stack.append(globalobj)
|
|
elif obj is ROOT:
|
|
stack.append(self.__root)
|
|
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.
|
|
stack.append(globalobj.find_parent(clip) or UNDEFINED)
|
|
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
|
|
|
|
self.vprint(f"{prefix}Bytecode engine finished.", component="bytecode")
|
|
|
|
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} shape into object slot {tag.id}",
|
|
component="tags",
|
|
)
|
|
|
|
if tag.reference not in self.shapes:
|
|
raise Exception(f"Cannot find shape reference {tag.reference}!")
|
|
|
|
self.__registered_objects[tag.id] = RegisteredShape(
|
|
tag.id,
|
|
tag.reference,
|
|
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, AP2ImageTag):
|
|
self.vprint(
|
|
f"{prefix} Loading {tag.reference} image into object slot {tag.id}",
|
|
component="tags",
|
|
)
|
|
|
|
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
|
|
|
|
elif isinstance(tag, AP2DefineSpriteTag):
|
|
self.vprint(
|
|
f"{prefix} Loading anonymous sprite into object slot {tag.id}",
|
|
component="tags",
|
|
)
|
|
|
|
# Register a new clip that we might reference to execute.
|
|
self.__registered_objects[tag.id] = RegisteredClip(
|
|
tag.id, tag.frames, tag.tags, tag.labels
|
|
)
|
|
|
|
# Didn't place a new clip, didn't change anything.
|
|
return None, False
|
|
|
|
elif isinstance(tag, AP2PlaceObjectTag):
|
|
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!"
|
|
)
|
|
|
|
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_hsl_shift = tag.hsl_shift or obj.hsl_shift
|
|
new_transform = (
|
|
obj.transform.update(tag.transform)
|
|
if tag.transform is not None
|
|
else obj.transform
|
|
)
|
|
new_rotation_origin = tag.rotation_origin or obj.rotation_origin
|
|
new_blend = tag.blend or obj.blend
|
|
new_projection = (
|
|
tag.projection
|
|
if tag.projection != AP2PlaceObjectTag.PROJECTION_NONE
|
|
else obj.projection
|
|
)
|
|
|
|
if (
|
|
tag.source_tag_id is not None
|
|
and tag.source_tag_id != obj.source.tag_id
|
|
):
|
|
# This completely updates the pointed-at object.
|
|
newobj = self.__registered_objects[tag.source_tag_id]
|
|
self.vprint(
|
|
f"{prefix} Replacing Object source {obj.source.tag_id} ({obj.source.reference}) with {tag.source_tag_id} ({newobj.reference}) on object with Object ID {tag.object_id} onto Depth {tag.depth}",
|
|
component="tags",
|
|
)
|
|
|
|
if isinstance(newobj, RegisteredShape):
|
|
operating_clip.placed_objects[i] = PlacedShape(
|
|
obj.object_id,
|
|
obj.depth,
|
|
new_rotation_origin,
|
|
new_transform,
|
|
new_projection,
|
|
new_mult_color,
|
|
new_add_color,
|
|
new_hsl_shift,
|
|
new_blend,
|
|
obj.mask,
|
|
newobj,
|
|
)
|
|
|
|
# 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,
|
|
new_rotation_origin,
|
|
new_transform,
|
|
new_projection,
|
|
new_mult_color,
|
|
new_add_color,
|
|
new_hsl_shift,
|
|
new_blend,
|
|
obj.mask,
|
|
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_origin,
|
|
new_transform,
|
|
new_projection,
|
|
new_mult_color,
|
|
new_add_color,
|
|
new_hsl_shift,
|
|
new_blend,
|
|
obj.mask,
|
|
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_origin,
|
|
new_transform,
|
|
new_projection,
|
|
new_mult_color,
|
|
new_add_color,
|
|
new_hsl_shift,
|
|
new_blend,
|
|
obj.mask,
|
|
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} ({obj.source.reference}) on Depth {tag.depth}",
|
|
component="tags",
|
|
)
|
|
obj.mult_color = new_mult_color
|
|
obj.add_color = new_add_color
|
|
obj.hsl_shift = new_hsl_shift
|
|
obj.transform = new_transform
|
|
obj.rotation_origin = new_rotation_origin
|
|
obj.projection = new_projection
|
|
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:
|
|
newobj = self.__registered_objects[tag.source_tag_id]
|
|
self.vprint(
|
|
f"{prefix} Placing Object {tag.source_tag_id} ({newobj.reference}) with Object ID {tag.object_id} onto Depth {tag.depth}",
|
|
component="tags",
|
|
)
|
|
|
|
if isinstance(newobj, RegisteredShape):
|
|
operating_clip.placed_objects.append(
|
|
PlacedShape(
|
|
tag.object_id,
|
|
tag.depth,
|
|
tag.rotation_origin or Point.identity(),
|
|
tag.transform or Matrix.identity(),
|
|
tag.projection,
|
|
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.hsl_shift or HSL(0.0, 0.0, 0.0),
|
|
tag.blend or 0,
|
|
None,
|
|
newobj,
|
|
)
|
|
)
|
|
|
|
# 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,
|
|
tag.rotation_origin or Point.identity(),
|
|
tag.transform or Matrix.identity(),
|
|
tag.projection,
|
|
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.hsl_shift or HSL(0.0, 0.0, 0.0),
|
|
tag.blend or 0,
|
|
None,
|
|
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_origin or Point.identity(),
|
|
tag.transform or Matrix.identity(),
|
|
tag.projection,
|
|
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.hsl_shift or HSL(0.0, 0.0, 0.0),
|
|
tag.blend or 0,
|
|
None,
|
|
newobj,
|
|
)
|
|
operating_clip.placed_objects.append(placed_clip)
|
|
|
|
for flags, code in tag.triggers.items():
|
|
if flags & AP2Trigger.ON_LOAD:
|
|
for bytecode in code:
|
|
self.__execute_bytecode(
|
|
bytecode, placed_clip, prefix=prefix + " "
|
|
)
|
|
else:
|
|
print(
|
|
"WARNING: Unhandled PLACE_OBJECT trigger with flags {flags}!"
|
|
)
|
|
|
|
# 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_origin or Point.identity(),
|
|
tag.transform or Matrix.identity(),
|
|
tag.projection,
|
|
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.hsl_shift or HSL(0.0, 0.0, 0.0),
|
|
tag.blend or 0,
|
|
None,
|
|
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}",
|
|
component="tags",
|
|
)
|
|
|
|
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}!"
|
|
)
|
|
|
|
# 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.
|
|
|
|
# Didn't place a new clip, changed parent clip.
|
|
return None, True
|
|
|
|
elif isinstance(tag, AP2DoActionTag):
|
|
self.vprint(f"{prefix} Execution action tag.", component="tags")
|
|
self.__execute_bytecode(
|
|
tag.bytecode, operating_clip, prefix=prefix + " "
|
|
)
|
|
|
|
# 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, AP2DefineMorphShapeTag):
|
|
print("WARNING: Unhandled DEFINE_MORPH_SHAPE tag!")
|
|
|
|
self.__registered_objects[tag.id] = RegisteredDummy(
|
|
tag.id,
|
|
)
|
|
|
|
# Didn't place a new clip.
|
|
return None, False
|
|
|
|
elif isinstance(tag, AP2PlaceCameraTag):
|
|
self.vprint(f"{prefix} Place camera tag.", component="tags")
|
|
self.__camera = PlacedCamera(
|
|
tag.center,
|
|
tag.focal_length,
|
|
)
|
|
|
|
# Didn't place a new clip.
|
|
return None, False
|
|
|
|
else:
|
|
raise Exception(f"Failed to process tag: {tag}")
|
|
|
|
def __apply_mask(
|
|
self,
|
|
parent_mask: Image.Image,
|
|
transform: Matrix,
|
|
projection: int,
|
|
mask: Mask,
|
|
) -> Image.Image:
|
|
if mask.rectangle is None:
|
|
# Calculate the new mask rectangle.
|
|
mask.rectangle = affine_composite(
|
|
Image.new(
|
|
"RGBA",
|
|
(int(mask.bounds.right), int(mask.bounds.bottom)),
|
|
(0, 0, 0, 0),
|
|
),
|
|
Color(0.0, 0.0, 0.0, 0.0),
|
|
Color(1.0, 1.0, 1.0, 1.0),
|
|
HSL(0.0, 0.0, 0.0),
|
|
Matrix.identity().translate(Point(mask.bounds.left, mask.bounds.top)),
|
|
None,
|
|
0,
|
|
Image.new(
|
|
"RGBA",
|
|
(int(mask.bounds.width), int(mask.bounds.height)),
|
|
(255, 0, 0, 255),
|
|
),
|
|
single_threaded=self.__single_threaded,
|
|
aa_mode=AAMode.NONE,
|
|
)
|
|
|
|
# Draw the mask onto a new image.
|
|
if projection == AP2PlaceObjectTag.PROJECTION_AFFINE:
|
|
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),
|
|
HSL(0.0, 0.0, 0.0),
|
|
transform,
|
|
None,
|
|
257,
|
|
mask.rectangle,
|
|
single_threaded=self.__single_threaded,
|
|
aa_mode=AAMode.NONE,
|
|
)
|
|
elif projection == AP2PlaceObjectTag.PROJECTION_PERSPECTIVE:
|
|
if self.__camera is None:
|
|
print(
|
|
"WARNING: Element requests perspective projection but no camera exists!"
|
|
)
|
|
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),
|
|
HSL(0.0, 0.0, 0.0),
|
|
transform,
|
|
None,
|
|
257,
|
|
mask.rectangle,
|
|
single_threaded=self.__single_threaded,
|
|
aa_mode=AAMode.NONE,
|
|
)
|
|
else:
|
|
calculated_mask = perspective_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),
|
|
HSL(0.0, 0.0, 0.0),
|
|
transform,
|
|
self.__camera.center,
|
|
self.__camera.focal_length,
|
|
None,
|
|
257,
|
|
mask.rectangle,
|
|
single_threaded=self.__single_threaded,
|
|
aa_mode=AAMode.NONE,
|
|
)
|
|
|
|
# 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),
|
|
HSL(0.0, 0.0, 0.0),
|
|
Matrix.identity(),
|
|
None,
|
|
256,
|
|
calculated_mask,
|
|
single_threaded=self.__single_threaded,
|
|
aa_mode=AAMode.NONE,
|
|
)
|
|
|
|
def __render_object(
|
|
self,
|
|
img: Image.Image,
|
|
renderable: PlacedObject,
|
|
parent_transform: Matrix,
|
|
parent_projection: int,
|
|
parent_mask: Image.Image,
|
|
parent_mult_color: Color,
|
|
parent_add_color: Color,
|
|
parent_hsl_shift: HSL,
|
|
parent_blend: int,
|
|
only_depths: Optional[List[int]] = None,
|
|
prefix: str = "",
|
|
) -> Image.Image:
|
|
if not renderable.visible:
|
|
self.vprint(
|
|
f"{prefix} Ignoring invisible placed object ID {renderable.object_id} from sprite {renderable.source.tag_id} ({renderable.source.reference}) on Depth {renderable.depth}",
|
|
component="render",
|
|
)
|
|
return img
|
|
|
|
self.vprint(
|
|
f"{prefix} Rendering placed object ID {renderable.object_id} from sprite {renderable.source.tag_id} ({renderable.source.reference}) onto Depth {renderable.depth}",
|
|
component="render",
|
|
)
|
|
|
|
# Compute the affine transformation matrix for this object.
|
|
transform = renderable.transform.multiply(parent_transform).translate(
|
|
Point.identity().subtract(renderable.rotation_origin)
|
|
)
|
|
projection = (
|
|
AP2PlaceObjectTag.PROJECTION_PERSPECTIVE
|
|
if parent_projection == AP2PlaceObjectTag.PROJECTION_PERSPECTIVE
|
|
else renderable.projection
|
|
)
|
|
|
|
# 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)
|
|
)
|
|
hsl_shift = (renderable.hsl_shift or HSL(0.0, 0.0, 0.0)).add(parent_hsl_shift)
|
|
blend = renderable.blend or 0
|
|
if parent_blend not in {0, 1, 2} and blend in {0, 1, 2}:
|
|
blend = parent_blend
|
|
|
|
if renderable.mask:
|
|
mask = self.__apply_mask(
|
|
parent_mask, transform, projection, renderable.mask
|
|
)
|
|
else:
|
|
mask = parent_mask
|
|
|
|
if projection == AP2PlaceObjectTag.PROJECTION_AFFINE:
|
|
projection_string = "affine projection"
|
|
elif projection == AP2PlaceObjectTag.PROJECTION_PERSPECTIVE:
|
|
projection_string = "perspective projection"
|
|
else:
|
|
projection_string = "no projection"
|
|
|
|
if blend == 3:
|
|
blend_string = "multiply"
|
|
elif blend == 8:
|
|
blend_string = "addition"
|
|
elif blend == 9 or blend == 70:
|
|
blend_string = "subtraction"
|
|
elif blend == 13:
|
|
blend_string = "overlay"
|
|
else:
|
|
blend_string = "normal"
|
|
|
|
# Render individual shapes if this is a sprite.
|
|
if isinstance(renderable, PlacedClip):
|
|
new_only_depths: Optional[List[int]] = None
|
|
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
|
|
|
|
self.vprint(
|
|
f"{prefix} Rendered object uses {projection_string} with transform [{transform}]",
|
|
component="render",
|
|
)
|
|
self.vprint(
|
|
f"{prefix} Rendered object uses {blend_string} with {mult_color} and {add_color} colors",
|
|
component="render",
|
|
)
|
|
self.vprint(
|
|
f"{prefix} Rendered object applies a HSL shift of {hsl_shift}",
|
|
component="render",
|
|
)
|
|
|
|
# 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,
|
|
projection,
|
|
mask,
|
|
mult_color,
|
|
add_color,
|
|
hsl_shift,
|
|
blend,
|
|
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
|
|
|
|
self.vprint(
|
|
f"{prefix} Rendered object uses {projection_string} with transform [{transform}]",
|
|
component="render",
|
|
)
|
|
self.vprint(
|
|
f"{prefix} Rendered object uses {blend_string} with {mult_color} and {add_color} colors",
|
|
component="render",
|
|
)
|
|
self.vprint(
|
|
f"{prefix} Rendered object applies a HSL shift of {hsl_shift}",
|
|
component="render",
|
|
)
|
|
|
|
# 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
|
|
rectangle = False
|
|
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!")
|
|
if params.blend is None:
|
|
raise Exception(
|
|
"Logic error, rectangle without a blend color!"
|
|
)
|
|
|
|
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
|
|
rectangle = True
|
|
|
|
if texture is not None:
|
|
if projection == AP2PlaceObjectTag.PROJECTION_AFFINE:
|
|
if self.__enable_aa:
|
|
aamode = (
|
|
AAMode.UNSCALED_SSAA_ONLY
|
|
if rectangle
|
|
else AAMode.SSAA_OR_BILINEAR
|
|
)
|
|
else:
|
|
aamode = AAMode.NONE
|
|
|
|
img = affine_composite(
|
|
img,
|
|
add_color,
|
|
mult_color,
|
|
hsl_shift,
|
|
transform,
|
|
mask,
|
|
blend,
|
|
texture,
|
|
single_threaded=self.__single_threaded,
|
|
aa_mode=aamode,
|
|
)
|
|
elif projection == AP2PlaceObjectTag.PROJECTION_PERSPECTIVE:
|
|
if self.__camera is None:
|
|
if self.__enable_aa:
|
|
aamode = (
|
|
AAMode.UNSCALED_SSAA_ONLY
|
|
if rectangle
|
|
else AAMode.SSAA_OR_BILINEAR
|
|
)
|
|
else:
|
|
aamode = AAMode.NONE
|
|
|
|
print(
|
|
"WARNING: Element requests perspective projection but no camera exists!"
|
|
)
|
|
img = affine_composite(
|
|
img,
|
|
add_color,
|
|
mult_color,
|
|
hsl_shift,
|
|
transform,
|
|
mask,
|
|
blend,
|
|
texture,
|
|
single_threaded=self.__single_threaded,
|
|
aa_mode=aamode,
|
|
)
|
|
else:
|
|
if self.__enable_aa:
|
|
aamode = (
|
|
AAMode.UNSCALED_SSAA_ONLY
|
|
if rectangle
|
|
else AAMode.SSAA_ONLY
|
|
)
|
|
else:
|
|
aamode = AAMode.NONE
|
|
|
|
img = perspective_composite(
|
|
img,
|
|
add_color,
|
|
mult_color,
|
|
hsl_shift,
|
|
transform,
|
|
self.__camera.center,
|
|
self.__camera.focal_length,
|
|
mask,
|
|
blend,
|
|
texture,
|
|
single_threaded=self.__single_threaded,
|
|
aa_mode=aamode,
|
|
)
|
|
|
|
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
|
|
|
|
self.vprint(
|
|
f"{prefix} Rendered object uses {projection_string} with transform [{transform}]",
|
|
component="render",
|
|
)
|
|
self.vprint(
|
|
f"{prefix} Rendered object uses {blend_string} with {mult_color} and {add_color} colors",
|
|
component="render",
|
|
)
|
|
self.vprint(
|
|
f"{prefix} Rendered object applies a HSL shift of {hsl_shift}",
|
|
component="render",
|
|
)
|
|
|
|
# This is a shape draw reference.
|
|
texture = self.textures[renderable.source.reference]
|
|
if projection == AP2PlaceObjectTag.PROJECTION_AFFINE:
|
|
img = affine_composite(
|
|
img,
|
|
add_color,
|
|
mult_color,
|
|
hsl_shift,
|
|
transform,
|
|
mask,
|
|
blend,
|
|
texture,
|
|
single_threaded=self.__single_threaded,
|
|
aa_mode=AAMode.SSAA_OR_BILINEAR
|
|
if self.__enable_aa
|
|
else AAMode.NONE,
|
|
)
|
|
elif projection == AP2PlaceObjectTag.PROJECTION_PERSPECTIVE:
|
|
if self.__camera is None:
|
|
print(
|
|
"WARNING: Element requests perspective projection but no camera exists!"
|
|
)
|
|
img = affine_composite(
|
|
img,
|
|
add_color,
|
|
mult_color,
|
|
hsl_shift,
|
|
transform,
|
|
mask,
|
|
blend,
|
|
texture,
|
|
single_threaded=self.__single_threaded,
|
|
aa_mode=AAMode.SSAA_OR_BILINEAR
|
|
if self.__enable_aa
|
|
else AAMode.NONE,
|
|
)
|
|
else:
|
|
img = perspective_composite(
|
|
img,
|
|
add_color,
|
|
mult_color,
|
|
hsl_shift,
|
|
transform,
|
|
self.__camera.center,
|
|
self.__camera.focal_length,
|
|
mask,
|
|
blend,
|
|
texture,
|
|
single_threaded=self.__single_threaded,
|
|
aa_mode=AAMode.SSAA_ONLY if self.__enable_aa else AAMode.NONE,
|
|
)
|
|
elif isinstance(renderable, PlacedDummy):
|
|
# Nothing to do!
|
|
pass
|
|
else:
|
|
raise Exception(f"Unknown placed object type to render {renderable}!")
|
|
|
|
return img
|
|
|
|
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}",
|
|
component="tags",
|
|
)
|
|
|
|
# Track whether anything in ourselves or our children changes during this processing.
|
|
changed = False
|
|
|
|
# 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 only_dirty
|
|
or (clip.finished and clip is self.__root)
|
|
):
|
|
# 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
|
|
elif clip.finished:
|
|
# Rewind the clip to the beginning, loop it.
|
|
clip.rewind()
|
|
clip.requested_frame = clip.frame + 1
|
|
else:
|
|
# We need to do as many things as we need to get to the next frame.
|
|
clip.requested_frame = clip.frame + 1
|
|
|
|
while True:
|
|
# 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.
|
|
if clip is self.__root:
|
|
print(
|
|
"WARNING: Root clip was rewound, its possible this animation plays forever!"
|
|
)
|
|
clip.rewind()
|
|
|
|
self.vprint(
|
|
f"{prefix} Processing frame {clip.frame} on our way to frame {clip.requested_frame}",
|
|
component="tags",
|
|
)
|
|
|
|
# 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 we need to move forward to a new frame.
|
|
if clip.frame != clip.requested_frame:
|
|
frame = clip.source.frames[clip.frame]
|
|
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",
|
|
component="tags",
|
|
)
|
|
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
|
|
]
|
|
|
|
# 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
|
|
)
|
|
]
|
|
)
|
|
for tagno, tag in enumerate(tags):
|
|
# Perform the action of this tag.
|
|
self.vprint(
|
|
f"{prefix} Sprite Tag ID: {clip.source.tag_id} ({clip.source.reference}), Current Tag: {frame.start_tag_offset + tagno}, Num Tags: {frame.num_tags}",
|
|
component="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:
|
|
# 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()
|
|
|
|
# Now, handle each of the existing clips.
|
|
for child in child_clips:
|
|
changed = (
|
|
self.__process_tags(child, only_dirty, prefix=prefix + " ")
|
|
or changed
|
|
)
|
|
|
|
# See if we're done with this clip.
|
|
if clip.frame == clip.requested_frame:
|
|
clip.requested_frame = None
|
|
break
|
|
|
|
self.vprint(
|
|
f"{prefix}Finished handling {'dirty updates on ' if only_dirty else ''}placed clip {clip.object_id} at depth {clip.depth}",
|
|
component="tags",
|
|
)
|
|
|
|
# Return if anything was modified.
|
|
return changed
|
|
|
|
def __handle_imports(
|
|
self, swf: SWF
|
|
) -> Dict[
|
|
int, Union[RegisteredShape, RegisteredClip, RegisteredImage, RegisteredDummy]
|
|
]:
|
|
external_objects: Dict[
|
|
int,
|
|
Union[RegisteredShape, RegisteredClip, RegisteredImage, 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:
|
|
# 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!"
|
|
)
|
|
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, RegisteredImage, 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,
|
|
swf.labels,
|
|
)
|
|
|
|
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, RegisteredImage, 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 tag in 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,
|
|
tag.reference,
|
|
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, 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,
|
|
)
|
|
|
|
elif isinstance(tag, AP2DefineSpriteTag):
|
|
new_clip = RegisteredClip(tag.id, tag.frames, tag.tags, tag.labels)
|
|
|
|
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]],
|
|
only_frames: Optional[List[int]],
|
|
movie_transform: Matrix,
|
|
background_image: Optional[List[Image.Image]],
|
|
overridden_width: Optional[float],
|
|
overridden_height: Optional[float],
|
|
) -> Generator[Image.Image, None, None]:
|
|
# First, let's attempt to resolve imports.
|
|
self.__registered_objects = self.__handle_imports(swf)
|
|
|
|
# Initialize overall frame advancement stuff.
|
|
last_rendered_frame: Optional[Image.Image] = None
|
|
frameno: int = 0
|
|
|
|
# Calculate actual size based on given movie transform.
|
|
actual_width = overridden_width or swf.location.width
|
|
actual_height = overridden_height or swf.location.height
|
|
resized_width, resized_height, _ = movie_transform.multiply_point(
|
|
Point(actual_width, actual_height)
|
|
).as_tuple()
|
|
|
|
if round(swf.location.top, 2) != 0.0 or round(swf.location.left, 2) != 0.0:
|
|
# TODO: If the location top/left is nonzero, we need move the root transform
|
|
# so that the correct viewport is rendered.
|
|
print("WARNING: Root clip requested to play not in top-left corner!")
|
|
|
|
# Create a root clip for the movie to play.
|
|
root_clip = PlacedClip(
|
|
-1,
|
|
-1,
|
|
Point.identity(),
|
|
Matrix.identity(),
|
|
AP2PlaceObjectTag.PROJECTION_AFFINE,
|
|
Color(1.0, 1.0, 1.0, 1.0),
|
|
Color(0.0, 0.0, 0.0, 0.0),
|
|
HSL(0.0, 0.0, 0.0),
|
|
0,
|
|
None,
|
|
RegisteredClip(
|
|
None,
|
|
swf.frames,
|
|
swf.tags,
|
|
swf.labels,
|
|
),
|
|
)
|
|
root_clip._width = int(actual_width)
|
|
root_clip._height = int(actual_height)
|
|
last_width = actual_width
|
|
last_height = actual_height
|
|
self.__root = root_clip
|
|
|
|
# If we have a background image, add it to the root clip.
|
|
background_object = RegisteredImage(-1, "INVALID_REFERENCE_NAME")
|
|
background_container: Optional[PlacedImage] = None
|
|
background_frames = 0
|
|
|
|
if background_image:
|
|
# Stretch the images to make sure they fit the entire frame.
|
|
imgwidth = background_image[0].width
|
|
imgheight = background_image[0].height
|
|
background_matrix = Matrix.affine(
|
|
a=actual_width / imgwidth,
|
|
b=0,
|
|
c=0,
|
|
d=actual_height / imgheight,
|
|
tx=0,
|
|
ty=0,
|
|
)
|
|
background_frames = len(background_image)
|
|
|
|
# Register the background images with the texture library.
|
|
for background_frame in range(background_frames):
|
|
if (
|
|
background_image[background_frame].width != imgwidth
|
|
or background_image[background_frame].height != imgheight
|
|
):
|
|
raise Exception(
|
|
f"Frame {background_frame + 1} of background image sequence has different dimensions than others!"
|
|
)
|
|
name = f"{swf.exported_name}_inserted_background_{background_frame}"
|
|
self.textures[name] = background_image[background_frame].convert("RGBA")
|
|
|
|
# Place an instance of this background on the root clip.
|
|
background_container = PlacedImage(
|
|
-1,
|
|
-1,
|
|
Point.identity(),
|
|
background_matrix,
|
|
AP2PlaceObjectTag.PROJECTION_AFFINE,
|
|
Color(1.0, 1.0, 1.0, 1.0),
|
|
Color(0.0, 0.0, 0.0, 0.0),
|
|
HSL(0.0, 0.0, 0.0),
|
|
0,
|
|
None,
|
|
background_object,
|
|
)
|
|
root_clip.placed_objects.append(background_container)
|
|
|
|
# Create the root mask for where to draw the root clip.
|
|
movie_mask = Image.new(
|
|
"RGBA", (resized_width, resized_height), color=(255, 0, 0, 255)
|
|
)
|
|
|
|
# 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)
|
|
actual_hsl_shift = HSL(0.0, 0.0, 0.0)
|
|
actual_blend = 0
|
|
|
|
max_frame: Optional[int] = None
|
|
if only_frames:
|
|
max_frame = max(only_frames)
|
|
|
|
# Now play the frames of the root clip.
|
|
try:
|
|
while root_clip.playing and not root_clip.finished:
|
|
# Create a new image to render into.
|
|
self.vprint(
|
|
f"Rendering frame {frameno + 1}/{len(root_clip.source.frames)}",
|
|
component="core",
|
|
)
|
|
|
|
# Go through all registered clips, place all needed tags.
|
|
changed = self.__process_tags(root_clip, False)
|
|
while self.__is_dirty(root_clip):
|
|
changed = self.__process_tags(root_clip, True) or changed
|
|
|
|
# Calculate a new background frame if needed.
|
|
if background_container is not None and background_frames > 0:
|
|
# First, make sure we're still placed in the root clip, which can be undone
|
|
# if it is rewound.
|
|
for obj in root_clip.placed_objects:
|
|
if obj is background_container:
|
|
break
|
|
else:
|
|
self.vprint(
|
|
"Root clip was rewound, re-placing background image on clip."
|
|
)
|
|
root_clip.placed_objects.append(background_container)
|
|
|
|
# Now, update the background image if we need to.
|
|
background_frame = frameno % background_frames
|
|
name = f"{swf.exported_name}_inserted_background_{background_frame}"
|
|
if background_object.reference != name:
|
|
background_object.reference = name
|
|
changed = True
|
|
|
|
# Adjust camera based on the movie's scaling.
|
|
if self.__camera is not None and not self.__camera.adjusted:
|
|
self.__camera.center = movie_transform.multiply_point(
|
|
self.__camera.center
|
|
)
|
|
self.__camera.adjusted = True
|
|
|
|
# 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)}",
|
|
component="core",
|
|
)
|
|
last_rendered_frame = None
|
|
frameno += 1
|
|
continue
|
|
|
|
if changed or last_rendered_frame is None:
|
|
if (
|
|
last_width != root_clip._width
|
|
or last_height != root_clip._height
|
|
):
|
|
last_width = root_clip._width
|
|
last_height = root_clip._height
|
|
if (
|
|
root_clip._width > actual_width
|
|
or root_clip._height > actual_height
|
|
):
|
|
print(
|
|
f"WARNING: Root clip requested to resize to {last_width}x{last_height} which overflows root canvas!"
|
|
)
|
|
|
|
# Now, render out the placed objects.
|
|
color = swf.color or Color(0.0, 0.0, 0.0, 0.0)
|
|
curimage = Image.new(
|
|
"RGBA", (resized_width, resized_height), color=color.as_tuple()
|
|
)
|
|
curimage = self.__render_object(
|
|
curimage,
|
|
root_clip,
|
|
movie_transform,
|
|
AP2PlaceObjectTag.PROJECTION_AFFINE,
|
|
movie_mask,
|
|
actual_mult_color,
|
|
actual_add_color,
|
|
actual_hsl_shift,
|
|
actual_blend,
|
|
only_depths=only_depths,
|
|
)
|
|
else:
|
|
# Nothing changed, make a copy of the previous render.
|
|
self.vprint(" Using previous frame render", component="core")
|
|
curimage = last_rendered_frame.copy()
|
|
|
|
# Return that frame, advance our bookkeeping.
|
|
self.vprint(
|
|
f"Finished rendering frame {frameno + 1}/{len(root_clip.source.frames)}",
|
|
component="core",
|
|
)
|
|
last_rendered_frame = curimage
|
|
frameno += 1
|
|
yield curimage
|
|
|
|
# See if we should bail because we passed the last requested frame.
|
|
if max_frame is not None and frameno == max_frame:
|
|
break
|
|
except KeyboardInterrupt:
|
|
# Allow ctrl-c to end early and render a partial animation.
|
|
print(
|
|
f"WARNING: Interrupted early, will render only {frameno}/{len(root_clip.source.frames)} frames of animation!"
|
|
)
|
|
|
|
# Clean up
|
|
self.__root = None
|