Implement the first bits of bytecode processing, including the ability to go to an animation frame.
This commit is contained in:
parent
0a6635d993
commit
14c8ac9347
@ -1,9 +1,10 @@
|
|||||||
from typing import Dict, List, Tuple, Optional, Union
|
from typing import Any, Dict, List, Tuple, Optional, Union
|
||||||
from PIL import Image # type: ignore
|
from PIL import Image # type: ignore
|
||||||
|
|
||||||
from .blend import affine_composite
|
from .blend import affine_composite
|
||||||
from .swf import SWF, Frame, Tag, AP2ShapeTag, AP2DefineSpriteTag, AP2PlaceObjectTag, AP2RemoveObjectTag, AP2DoActionTag, AP2DefineFontTag, AP2DefineEditTextTag, AP2PlaceCameraTag
|
from .swf import SWF, Frame, Tag, AP2ShapeTag, AP2DefineSpriteTag, AP2PlaceObjectTag, AP2RemoveObjectTag, AP2DoActionTag, AP2DefineFontTag, AP2DefineEditTextTag, AP2PlaceCameraTag
|
||||||
from .types import Color, Matrix, Point, Rectangle
|
from .decompile import ByteCode
|
||||||
|
from .types import Color, Matrix, Point, Rectangle, AP2Trigger, AP2Action, PushAction, StoreRegisterAction, StringConstant, Register, THIS, UNDEFINED
|
||||||
from .geo import Shape, DrawParams
|
from .geo import Shape, DrawParams
|
||||||
from .util import VerboseOutput
|
from .util import VerboseOutput
|
||||||
|
|
||||||
@ -47,7 +48,7 @@ class RegisteredDummy:
|
|||||||
|
|
||||||
class PlacedObject:
|
class PlacedObject:
|
||||||
# An object that occupies the screen at some depth.
|
# 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:
|
def __init__(self, object_id: int, depth: int, rotation_offset: Point, transform: Matrix, mult_color: Color, add_color: Color, blend: int, mask: Optional[Rectangle]) -> None:
|
||||||
self.__object_id = object_id
|
self.__object_id = object_id
|
||||||
self.__depth = depth
|
self.__depth = depth
|
||||||
self.rotation_offset = rotation_offset
|
self.rotation_offset = rotation_offset
|
||||||
@ -55,6 +56,7 @@ class PlacedObject:
|
|||||||
self.mult_color = mult_color
|
self.mult_color = mult_color
|
||||||
self.add_color = add_color
|
self.add_color = add_color
|
||||||
self.blend = blend
|
self.blend = blend
|
||||||
|
self.mask = mask
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def source(self) -> Union[RegisteredClip, RegisteredShape, RegisteredDummy]:
|
def source(self) -> Union[RegisteredClip, RegisteredShape, RegisteredDummy]:
|
||||||
@ -75,8 +77,19 @@ class PlacedObject:
|
|||||||
class PlacedShape(PlacedObject):
|
class PlacedShape(PlacedObject):
|
||||||
# A shape that occupies its parent clip at some depth. Placed by an AP2PlaceObjectTag
|
# A shape that occupies its parent clip at some depth. Placed by an AP2PlaceObjectTag
|
||||||
# referencing an AP2ShapeTag.
|
# 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:
|
def __init__(
|
||||||
super().__init__(object_id, depth, rotation_offset, transform, mult_color, add_color, blend)
|
self,
|
||||||
|
object_id: int,
|
||||||
|
depth: int,
|
||||||
|
rotation_offset: Point,
|
||||||
|
transform: Matrix,
|
||||||
|
mult_color: Color,
|
||||||
|
add_color: Color,
|
||||||
|
blend: int,
|
||||||
|
mask: Optional[Rectangle],
|
||||||
|
source: RegisteredShape,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(object_id, depth, rotation_offset, transform, mult_color, add_color, blend, mask)
|
||||||
self.__source = source
|
self.__source = source
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -90,11 +103,24 @@ class PlacedShape(PlacedObject):
|
|||||||
class PlacedClip(PlacedObject):
|
class PlacedClip(PlacedObject):
|
||||||
# A movieclip that occupies its parent clip at some depth. Placed by an AP2PlaceObjectTag
|
# A movieclip that occupies its parent clip at some depth. Placed by an AP2PlaceObjectTag
|
||||||
# referencing an AP2DefineSpriteTag. Essentially an embedded movie clip.
|
# 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:
|
def __init__(
|
||||||
super().__init__(object_id, depth, rotation_offset, transform, mult_color, add_color, blend)
|
self,
|
||||||
|
object_id: int,
|
||||||
|
depth: int,
|
||||||
|
rotation_offset: Point,
|
||||||
|
transform: Matrix,
|
||||||
|
mult_color: Color,
|
||||||
|
add_color: Color,
|
||||||
|
blend: int,
|
||||||
|
mask: Optional[Rectangle],
|
||||||
|
source: RegisteredClip,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(object_id, depth, rotation_offset, transform, mult_color, add_color, blend, mask)
|
||||||
self.placed_objects: List[PlacedObject] = []
|
self.placed_objects: List[PlacedObject] = []
|
||||||
self.frame: int = 0
|
self.frame: int = 0
|
||||||
self.__source = source
|
self.__source = source
|
||||||
|
self.playing: bool = True
|
||||||
|
self.requested_frame: Optional[int] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def source(self) -> RegisteredClip:
|
def source(self) -> RegisteredClip:
|
||||||
@ -111,11 +137,49 @@ class PlacedClip(PlacedObject):
|
|||||||
def __repr__(self) -> str:
|
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})"
|
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})"
|
||||||
|
|
||||||
|
# The following are attributes and functions necessary to support some simple bytecode.
|
||||||
|
def gotoAndPlay(self, frame: Any) -> None:
|
||||||
|
if not isinstance(frame, int):
|
||||||
|
# TODO: Technically this should also allow string labels to frames as identified in the
|
||||||
|
# SWF specification, but we don't support that here.
|
||||||
|
print(f"WARNING: Non-integer frame {frame} to gotoAndPlay function!")
|
||||||
|
return
|
||||||
|
if frame <= 0 or frame > len(self.source.frames):
|
||||||
|
return
|
||||||
|
self.requested_frame = frame
|
||||||
|
self.playing = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def frameOffset(self) -> int:
|
||||||
|
return self.requested_frame or self.frame
|
||||||
|
|
||||||
|
@frameOffset.setter
|
||||||
|
def frameOffset(self, val: Any) -> None:
|
||||||
|
if not isinstance(val, int):
|
||||||
|
# TODO: Technically this should also allow string labels to frames as identified in the
|
||||||
|
# SWF specification, but we don't support that here.
|
||||||
|
print(f"WARNING: Non-integer frameOffset {val} to frameOffset attribute!")
|
||||||
|
return
|
||||||
|
if val < 0 or val >= len(self.source.frames):
|
||||||
|
return
|
||||||
|
self.requested_frame = val + 1
|
||||||
|
|
||||||
|
|
||||||
class PlacedDummy(PlacedObject):
|
class PlacedDummy(PlacedObject):
|
||||||
# A reference to an object we can't find because we're missing the import.
|
# 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:
|
def __init__(
|
||||||
super().__init__(object_id, depth, rotation_offset, transform, mult_color, add_color, blend)
|
self,
|
||||||
|
object_id: int,
|
||||||
|
depth: int,
|
||||||
|
rotation_offset: Point,
|
||||||
|
transform: Matrix,
|
||||||
|
mult_color: Color,
|
||||||
|
add_color: Color,
|
||||||
|
blend: int,
|
||||||
|
mask: Optional[Rectangle],
|
||||||
|
source: RegisteredDummy,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(object_id, depth, rotation_offset, transform, mult_color, add_color, blend, mask)
|
||||||
self.__source = source
|
self.__source = source
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -123,6 +187,50 @@ class PlacedDummy(PlacedObject):
|
|||||||
return self.__source
|
return self.__source
|
||||||
|
|
||||||
|
|
||||||
|
class Movie:
|
||||||
|
def __init__(self, root: PlacedClip) -> None:
|
||||||
|
self.__root = root
|
||||||
|
|
||||||
|
def getInstanceAtDepth(self, depth: Any) -> Any:
|
||||||
|
if not isinstance(depth, int):
|
||||||
|
return UNDEFINED
|
||||||
|
|
||||||
|
# For some reason, it looks like internally the depth of all objects is
|
||||||
|
# stored added to -0x4000, so let's reverse that.
|
||||||
|
depth = depth + 0x4000
|
||||||
|
|
||||||
|
for obj in self.__root.placed_objects:
|
||||||
|
if obj.depth == depth:
|
||||||
|
return obj
|
||||||
|
|
||||||
|
print(f"WARNING: Could not find object at depth {depth}!")
|
||||||
|
return UNDEFINED
|
||||||
|
|
||||||
|
|
||||||
|
class AEPLib:
|
||||||
|
def __init__(self, this: PlacedObject, movie: Movie) -> None:
|
||||||
|
self.__this = this
|
||||||
|
self.__movie = movie
|
||||||
|
|
||||||
|
def aep_set_rect_mask(self, thisptr: Any, left: Any, right: Any, top: Any, bottom: Any) -> None:
|
||||||
|
if not isinstance(left, (int, float)) or not isinstance(right, (int, float)) or not isinstance(top, (int, float)) or not isinstance(bottom, (int, float)):
|
||||||
|
print("WARNING: Ignoring aeplib.aep_set_rect_mask call with invalid parameters!")
|
||||||
|
return
|
||||||
|
if thisptr is THIS:
|
||||||
|
self.__this.mask = Rectangle(
|
||||||
|
left=float(left),
|
||||||
|
right=float(right),
|
||||||
|
top=float(top),
|
||||||
|
bottom=float(bottom),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print("WARNING: Ignoring aeplib.aep_set_rect_mask call with unrecognized target!")
|
||||||
|
|
||||||
|
def aep_set_set_frame(self, thisptr: Any, frame: Any) -> None:
|
||||||
|
# I have no idea what this should do, so let's ignore it.
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class AFPRenderer(VerboseOutput):
|
class AFPRenderer(VerboseOutput):
|
||||||
def __init__(self, shapes: Dict[str, Shape] = {}, textures: Dict[str, Image.Image] = {}, swfs: Dict[str, SWF] = {}, single_threaded: bool = False) -> None:
|
def __init__(self, shapes: Dict[str, Shape] = {}, textures: Dict[str, Image.Image] = {}, swfs: Dict[str, SWF] = {}, single_threaded: bool = False) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -137,6 +245,7 @@ class AFPRenderer(VerboseOutput):
|
|||||||
|
|
||||||
# Internal render parameters.
|
# Internal render parameters.
|
||||||
self.__registered_objects: Dict[int, Union[RegisteredShape, RegisteredClip, RegisteredDummy]] = {}
|
self.__registered_objects: Dict[int, Union[RegisteredShape, RegisteredClip, RegisteredDummy]] = {}
|
||||||
|
self.__movie: Optional[Movie] = None
|
||||||
|
|
||||||
def add_shape(self, name: str, data: Shape) -> None:
|
def add_shape(self, name: str, data: Shape) -> None:
|
||||||
# Register a named shape with the renderer.
|
# Register a named shape with the renderer.
|
||||||
@ -193,6 +302,119 @@ class AFPRenderer(VerboseOutput):
|
|||||||
|
|
||||||
return paths
|
return paths
|
||||||
|
|
||||||
|
def __execute_bytecode(self, bytecode: ByteCode, clip: PlacedClip) -> None:
|
||||||
|
if self.__movie is None:
|
||||||
|
raise Exception("Logic error, executing bytecode outside of a rendering movie clip!")
|
||||||
|
|
||||||
|
location: int = 0
|
||||||
|
stack: List[Any] = []
|
||||||
|
variables: Dict[str, Any] = {
|
||||||
|
'aeplib': AEPLib(clip, self.__movie),
|
||||||
|
'GLOBAL': self.__movie,
|
||||||
|
}
|
||||||
|
registers: List[Any] = [UNDEFINED] * 256
|
||||||
|
|
||||||
|
while location < len(bytecode.actions):
|
||||||
|
action = bytecode.actions[location]
|
||||||
|
|
||||||
|
if action.opcode == AP2Action.END:
|
||||||
|
# End the execution.
|
||||||
|
self.vprint("Ending bytecode execution.")
|
||||||
|
break
|
||||||
|
elif action.opcode == AP2Action.GET_VARIABLE:
|
||||||
|
varname = stack.pop()
|
||||||
|
|
||||||
|
# Look up the variable, put it on the stack.
|
||||||
|
if varname in variables:
|
||||||
|
stack.append(variables[varname])
|
||||||
|
else:
|
||||||
|
stack.append(UNDEFINED)
|
||||||
|
elif action.opcode == AP2Action.SET_MEMBER:
|
||||||
|
# Grab what we're about to do.
|
||||||
|
set_value = stack.pop()
|
||||||
|
attribute = stack.pop()
|
||||||
|
obj = stack.pop()
|
||||||
|
|
||||||
|
if not hasattr(obj, attribute):
|
||||||
|
print(f"WARNING: Tried to set attribute {attribute} on {obj} but that attribute doesn't exist!")
|
||||||
|
else:
|
||||||
|
setattr(obj, attribute, set_value)
|
||||||
|
elif action.opcode == AP2Action.CALL_METHOD:
|
||||||
|
# Grab the method name.
|
||||||
|
methname = stack.pop()
|
||||||
|
|
||||||
|
# Grab the object to perform the call on.
|
||||||
|
obj = stack.pop()
|
||||||
|
|
||||||
|
# Grab the parameters to pass to the function.
|
||||||
|
num_params = stack.pop()
|
||||||
|
if not isinstance(num_params, int):
|
||||||
|
raise Exception("Logic error, cannot get number of parameters to method call!")
|
||||||
|
params = []
|
||||||
|
for _ in range(num_params):
|
||||||
|
params.append(stack.pop())
|
||||||
|
|
||||||
|
# Look up the python function we're calling.
|
||||||
|
try:
|
||||||
|
meth = getattr(obj, methname)
|
||||||
|
|
||||||
|
# Call it, set the return on the stack.
|
||||||
|
stack.append(meth(*params))
|
||||||
|
except AttributeError:
|
||||||
|
# Function does not exist!
|
||||||
|
print(f"WARNING: Tried to call {methname}({', '.join(repr(s) for s in params)}) on {obj} but that method doesn't exist!")
|
||||||
|
stack.append(UNDEFINED)
|
||||||
|
elif action.opcode == AP2Action.CALL_FUNCTION:
|
||||||
|
# Grab the method name.
|
||||||
|
funcname = stack.pop()
|
||||||
|
|
||||||
|
# Grab the object to perform the call on.
|
||||||
|
obj = variables['GLOBAL']
|
||||||
|
|
||||||
|
# Grab the parameters to pass to the function.
|
||||||
|
num_params = stack.pop()
|
||||||
|
if not isinstance(num_params, int):
|
||||||
|
raise Exception("Logic error, cannot get number of parameters to function call!")
|
||||||
|
params = []
|
||||||
|
for _ in range(num_params):
|
||||||
|
params.append(stack.pop())
|
||||||
|
|
||||||
|
# Look up the python function we're calling.
|
||||||
|
try:
|
||||||
|
func = getattr(obj, funcname)
|
||||||
|
|
||||||
|
# Call it, set the return on the stack.
|
||||||
|
stack.append(func(*params))
|
||||||
|
except AttributeError:
|
||||||
|
# Function does not exist!
|
||||||
|
print(f"WARNING: Tried to call {funcname}({', '.join(repr(s) for s in params)}) on {obj} but that function doesn't exist!")
|
||||||
|
stack.append(UNDEFINED)
|
||||||
|
elif isinstance(action, PushAction):
|
||||||
|
for obj in action.objects:
|
||||||
|
if isinstance(obj, Register):
|
||||||
|
stack.append(registers[obj.no])
|
||||||
|
elif isinstance(obj, StringConstant):
|
||||||
|
if obj.alias:
|
||||||
|
stack.append(obj.alias)
|
||||||
|
else:
|
||||||
|
stack.append(StringConstant.property_to_name(obj.const))
|
||||||
|
else:
|
||||||
|
stack.append(obj)
|
||||||
|
elif isinstance(action, StoreRegisterAction):
|
||||||
|
set_value = stack.pop()
|
||||||
|
if action.preserve_stack:
|
||||||
|
stack.append(set_value)
|
||||||
|
|
||||||
|
for reg in action.registers:
|
||||||
|
registers[reg.no] = set_value
|
||||||
|
elif action.opcode == AP2Action.POP:
|
||||||
|
stack.pop()
|
||||||
|
else:
|
||||||
|
print(f"WARNING: Unhandled opcode {action} with stack {stack}")
|
||||||
|
|
||||||
|
# Next opcode!
|
||||||
|
location += 1
|
||||||
|
|
||||||
def __place(self, tag: Tag, operating_clip: PlacedClip, prefix: str = "") -> Tuple[Optional[PlacedClip], bool]:
|
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,
|
# "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.
|
# such as defining a shape (registering it with our shape list) or adding/removing an object.
|
||||||
@ -253,6 +475,7 @@ class AFPRenderer(VerboseOutput):
|
|||||||
new_mult_color,
|
new_mult_color,
|
||||||
new_add_color,
|
new_add_color,
|
||||||
new_blend,
|
new_blend,
|
||||||
|
obj.mask,
|
||||||
newobj,
|
newobj,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -267,6 +490,7 @@ class AFPRenderer(VerboseOutput):
|
|||||||
new_mult_color,
|
new_mult_color,
|
||||||
new_add_color,
|
new_add_color,
|
||||||
new_blend,
|
new_blend,
|
||||||
|
obj.mask,
|
||||||
newobj,
|
newobj,
|
||||||
)
|
)
|
||||||
operating_clip.placed_objects[i] = new_clip
|
operating_clip.placed_objects[i] = new_clip
|
||||||
@ -282,6 +506,7 @@ class AFPRenderer(VerboseOutput):
|
|||||||
new_mult_color,
|
new_mult_color,
|
||||||
new_add_color,
|
new_add_color,
|
||||||
new_blend,
|
new_blend,
|
||||||
|
obj.mask,
|
||||||
newobj,
|
newobj,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -320,6 +545,7 @@ class AFPRenderer(VerboseOutput):
|
|||||||
tag.mult_color or Color(1.0, 1.0, 1.0, 1.0),
|
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.add_color or Color(0.0, 0.0, 0.0, 0.0),
|
||||||
tag.blend or 0,
|
tag.blend or 0,
|
||||||
|
None,
|
||||||
newobj,
|
newobj,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -327,14 +553,6 @@ class AFPRenderer(VerboseOutput):
|
|||||||
# Didn't place a new clip, changed the parent clip.
|
# Didn't place a new clip, changed the parent clip.
|
||||||
return None, True
|
return None, True
|
||||||
elif isinstance(newobj, RegisteredClip):
|
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(
|
placed_clip = PlacedClip(
|
||||||
tag.object_id,
|
tag.object_id,
|
||||||
tag.depth,
|
tag.depth,
|
||||||
@ -343,10 +561,18 @@ class AFPRenderer(VerboseOutput):
|
|||||||
tag.mult_color or Color(1.0, 1.0, 1.0, 1.0),
|
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.add_color or Color(0.0, 0.0, 0.0, 0.0),
|
||||||
tag.blend or 0,
|
tag.blend or 0,
|
||||||
|
None,
|
||||||
newobj,
|
newobj,
|
||||||
)
|
)
|
||||||
operating_clip.placed_objects.append(placed_clip)
|
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)
|
||||||
|
else:
|
||||||
|
print("WARNING: Unhandled PLACE_OBJECT trigger with flags {flags}!")
|
||||||
|
|
||||||
# Placed a new clip, changed the parent.
|
# Placed a new clip, changed the parent.
|
||||||
return placed_clip, True
|
return placed_clip, True
|
||||||
elif isinstance(newobj, RegisteredDummy):
|
elif isinstance(newobj, RegisteredDummy):
|
||||||
@ -359,6 +585,7 @@ class AFPRenderer(VerboseOutput):
|
|||||||
tag.mult_color or Color(1.0, 1.0, 1.0, 1.0),
|
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.add_color or Color(0.0, 0.0, 0.0, 0.0),
|
||||||
tag.blend or 0,
|
tag.blend or 0,
|
||||||
|
None,
|
||||||
newobj,
|
newobj,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -402,13 +629,14 @@ class AFPRenderer(VerboseOutput):
|
|||||||
if not removed_objects:
|
if not removed_objects:
|
||||||
print(f"WARNING: Couldn't find object to remove by ID {tag.object_id} and depth {tag.depth}!")
|
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.
|
# Didn't place a new clip, changed parent clip.
|
||||||
return None, True
|
return None, True
|
||||||
|
|
||||||
elif isinstance(tag, AP2DoActionTag):
|
elif isinstance(tag, AP2DoActionTag):
|
||||||
print("WARNING: Unhandled DO_ACTION tag!")
|
self.__execute_bytecode(tag.bytecode, operating_clip)
|
||||||
if self.verbose:
|
|
||||||
print(tag.bytecode.decompile())
|
|
||||||
|
|
||||||
# Didn't place a new clip.
|
# Didn't place a new clip.
|
||||||
return None, False
|
return None, False
|
||||||
@ -457,6 +685,9 @@ class AFPRenderer(VerboseOutput):
|
|||||||
if parent_blend not in {0, 1, 2} and blend in {0, 1, 2}:
|
if parent_blend not in {0, 1, 2} and blend in {0, 1, 2}:
|
||||||
blend = parent_blend
|
blend = parent_blend
|
||||||
|
|
||||||
|
if renderable.mask:
|
||||||
|
print(f"WARNING: Unsupported mask Rectangle({renderable.mask})!")
|
||||||
|
|
||||||
# Render individual shapes if this is a sprite.
|
# Render individual shapes if this is a sprite.
|
||||||
if isinstance(renderable, PlacedClip):
|
if isinstance(renderable, PlacedClip):
|
||||||
if only_depths is not None:
|
if only_depths is not None:
|
||||||
@ -553,30 +784,50 @@ class AFPRenderer(VerboseOutput):
|
|||||||
# Track whether anything in ourselves or our children changes during this processing.
|
# Track whether anything in ourselves or our children changes during this processing.
|
||||||
changed = False
|
changed = False
|
||||||
|
|
||||||
# Clips that are part of our own placed objects which we should handle.
|
while True:
|
||||||
child_clips = [c for c in clip.placed_objects if isinstance(c, PlacedClip)]
|
# See if this clip should actually be played.
|
||||||
|
if clip.requested_frame is None and not clip.playing:
|
||||||
|
break
|
||||||
|
|
||||||
# Execute each tag in the frame.
|
# See if we need to fast forward to a frame.
|
||||||
if not clip.finished:
|
if clip.requested_frame is not None:
|
||||||
frame = clip.source.frames[clip.frame]
|
if clip.frame > clip.requested_frame:
|
||||||
tags = clip.source.tags[frame.start_tag_offset:(frame.start_tag_offset + frame.num_tags)]
|
# Rewind this clip to the beginning so we can replay until the requested frame.
|
||||||
|
clip.placed_objects = []
|
||||||
|
clip.frame = 0
|
||||||
|
elif clip.frame == clip.requested_frame:
|
||||||
|
# We played up through the requested frame, we're done!
|
||||||
|
clip.requested_frame = None
|
||||||
|
break
|
||||||
|
|
||||||
for tagno, tag in enumerate(tags):
|
# Clips that are part of our own placed objects which we should handle.
|
||||||
# Perform the action of this tag.
|
child_clips = [c for c in clip.placed_objects if isinstance(c, PlacedClip)]
|
||||||
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.
|
# Execute each tag in the frame.
|
||||||
if new_clip:
|
if not clip.finished:
|
||||||
changed = self.__process_tags(new_clip, prefix=prefix + " ") or changed
|
frame = clip.source.frames[clip.frame]
|
||||||
|
tags = clip.source.tags[frame.start_tag_offset:(frame.start_tag_offset + frame.num_tags)]
|
||||||
|
|
||||||
# Now, handle each of the existing clips.
|
for tagno, tag in enumerate(tags):
|
||||||
for child in child_clips:
|
# Perform the action of this tag.
|
||||||
changed = self.__process_tags(child, prefix=prefix + " ") or changed
|
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
|
||||||
|
|
||||||
# Now, advance the frame for this clip.
|
# If we create a new movie clip, process it as well for this frame.
|
||||||
clip.advance()
|
if new_clip:
|
||||||
|
changed = self.__process_tags(new_clip, prefix=prefix + " ") or changed
|
||||||
|
|
||||||
|
# Now, handle each of the existing clips.
|
||||||
|
for child in child_clips:
|
||||||
|
changed = self.__process_tags(child, prefix=prefix + " ") or changed
|
||||||
|
|
||||||
|
# Now, advance the frame for this clip.
|
||||||
|
clip.advance()
|
||||||
|
|
||||||
|
# See if we should bail.
|
||||||
|
if clip.requested_frame is None:
|
||||||
|
break
|
||||||
|
|
||||||
self.vprint(f"{prefix}Finished handling placed clip {clip.object_id} at depth {clip.depth}")
|
self.vprint(f"{prefix}Finished handling placed clip {clip.object_id} at depth {clip.depth}")
|
||||||
|
|
||||||
@ -687,12 +938,14 @@ class AFPRenderer(VerboseOutput):
|
|||||||
Color(1.0, 1.0, 1.0, 1.0),
|
Color(1.0, 1.0, 1.0, 1.0),
|
||||||
Color(0.0, 0.0, 0.0, 0.0),
|
Color(0.0, 0.0, 0.0, 0.0),
|
||||||
0,
|
0,
|
||||||
|
None,
|
||||||
RegisteredClip(
|
RegisteredClip(
|
||||||
None,
|
None,
|
||||||
swf.frames,
|
swf.frames,
|
||||||
swf.tags,
|
swf.tags,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
self.__movie = Movie(root_clip)
|
||||||
|
|
||||||
# These could possibly be overwritten from an external source of we wanted.
|
# 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_mult_color = Color(1.0, 1.0, 1.0, 1.0)
|
||||||
@ -726,4 +979,7 @@ class AFPRenderer(VerboseOutput):
|
|||||||
# Allow ctrl-c to end early and render a partial animation.
|
# 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!")
|
print(f"WARNING: Interrupted early, will render only {len(frames)}/{len(root_clip.source.frames)} frames of animation!")
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
self.movie = None
|
||||||
|
|
||||||
return int(spf * 1000.0), frames
|
return int(spf * 1000.0), frames
|
||||||
|
@ -8,6 +8,7 @@ from .types import Matrix, Color, Point, Rectangle
|
|||||||
from .types import (
|
from .types import (
|
||||||
AP2Action,
|
AP2Action,
|
||||||
AP2Tag,
|
AP2Tag,
|
||||||
|
AP2Trigger,
|
||||||
DefineFunction2Action,
|
DefineFunction2Action,
|
||||||
InitRegisterAction,
|
InitRegisterAction,
|
||||||
StoreRegisterAction,
|
StoreRegisterAction,
|
||||||
@ -1235,33 +1236,33 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||||||
self.add_coverage(dataoffset + evt_offset, 8)
|
self.add_coverage(dataoffset + evt_offset, 8)
|
||||||
|
|
||||||
events: List[str] = []
|
events: List[str] = []
|
||||||
if evt_flags & 0x1:
|
if evt_flags & AP2Trigger.ON_LOAD:
|
||||||
events.append("ON_LOAD")
|
events.append("ON_LOAD")
|
||||||
if evt_flags & 0x2:
|
if evt_flags & AP2Trigger.ON_ENTER_FRAME:
|
||||||
events.append("ON_ENTER_FRAME")
|
events.append("ON_ENTER_FRAME")
|
||||||
if evt_flags & 0x4:
|
if evt_flags & AP2Trigger.ON_UNLOAD:
|
||||||
events.append("ON_UNLOAD")
|
events.append("ON_UNLOAD")
|
||||||
if evt_flags & 0x8:
|
if evt_flags & AP2Trigger.ON_MOUSE_MOVE:
|
||||||
events.append("ON_MOUSE_MOVE")
|
events.append("ON_MOUSE_MOVE")
|
||||||
if evt_flags & 0x10:
|
if evt_flags & AP2Trigger.ON_MOUSE_DOWN:
|
||||||
events.append("ON_MOUSE_DOWN")
|
events.append("ON_MOUSE_DOWN")
|
||||||
if evt_flags & 0x20:
|
if evt_flags & AP2Trigger.ON_MOUSE_UP:
|
||||||
events.append("ON_MOUSE_UP")
|
events.append("ON_MOUSE_UP")
|
||||||
if evt_flags & 0x40:
|
if evt_flags & AP2Trigger.ON_KEY_DOWN:
|
||||||
events.append("ON_KEY_DOWN")
|
events.append("ON_KEY_DOWN")
|
||||||
if evt_flags & 0x80:
|
if evt_flags & AP2Trigger.ON_KEY_UP:
|
||||||
events.append("ON_KEY_UP")
|
events.append("ON_KEY_UP")
|
||||||
if evt_flags & 0x100:
|
if evt_flags & AP2Trigger.ON_DATA:
|
||||||
events.append("ON_DATA")
|
events.append("ON_DATA")
|
||||||
if evt_flags & 0x400:
|
if evt_flags & AP2Trigger.ON_PRESS:
|
||||||
events.append("ON_PRESS")
|
events.append("ON_PRESS")
|
||||||
if evt_flags & 0x800:
|
if evt_flags & AP2Trigger.ON_RELEASE:
|
||||||
events.append("ON_RELEASE")
|
events.append("ON_RELEASE")
|
||||||
if evt_flags & 0x1000:
|
if evt_flags & AP2Trigger.ON_RELEASE_OUTSIDE:
|
||||||
events.append("ON_RELEASE_OUTSIDE")
|
events.append("ON_RELEASE_OUTSIDE")
|
||||||
if evt_flags & 0x2000:
|
if evt_flags & AP2Trigger.ON_ROLL_OVER:
|
||||||
events.append("ON_ROLL_OVER")
|
events.append("ON_ROLL_OVER")
|
||||||
if evt_flags & 0x4000:
|
if evt_flags & AP2Trigger.ON_ROLL_OUT:
|
||||||
events.append("ON_ROLL_OUT")
|
events.append("ON_ROLL_OUT")
|
||||||
|
|
||||||
bytecode_offset += evt_offset
|
bytecode_offset += evt_offset
|
||||||
|
@ -4,6 +4,7 @@ from .ap2 import (
|
|||||||
AP2Action,
|
AP2Action,
|
||||||
AP2Object,
|
AP2Object,
|
||||||
AP2Pointer,
|
AP2Pointer,
|
||||||
|
AP2Trigger,
|
||||||
DefineFunction2Action,
|
DefineFunction2Action,
|
||||||
Expression,
|
Expression,
|
||||||
GenericObject,
|
GenericObject,
|
||||||
@ -39,6 +40,7 @@ __all__ = [
|
|||||||
'AP2Action',
|
'AP2Action',
|
||||||
'AP2Object',
|
'AP2Object',
|
||||||
'AP2Pointer',
|
'AP2Pointer',
|
||||||
|
'AP2Trigger',
|
||||||
'DefineFunction2Action',
|
'DefineFunction2Action',
|
||||||
'Expression',
|
'Expression',
|
||||||
'GenericObject',
|
'GenericObject',
|
||||||
|
@ -2935,3 +2935,20 @@ class AP2Pointer:
|
|||||||
display_P = 0xb4
|
display_P = 0xb4
|
||||||
geom_P = 0xb5
|
geom_P = 0xb5
|
||||||
filtesr_P = 0xb6
|
filtesr_P = 0xb6
|
||||||
|
|
||||||
|
|
||||||
|
class AP2Trigger:
|
||||||
|
ON_LOAD = 0x1
|
||||||
|
ON_ENTER_FRAME = 0x2
|
||||||
|
ON_UNLOAD = 0x4
|
||||||
|
ON_MOUSE_MOVE = 0x8
|
||||||
|
ON_MOUSE_DOWN = 0x10
|
||||||
|
ON_MOUSE_UP = 0x20
|
||||||
|
ON_KEY_DOWN = 0x40
|
||||||
|
ON_KEY_UP = 0x80
|
||||||
|
ON_DATA = 0x100
|
||||||
|
ON_PRESS = 0x400
|
||||||
|
ON_RELEASE = 0x800
|
||||||
|
ON_RELEASE_OUTSIDE = 0x1000
|
||||||
|
ON_ROLL_OVER = 0x2000
|
||||||
|
ON_ROLL_OUT = 0x4000
|
||||||
|
Loading…
Reference in New Issue
Block a user