From 089eef15884cdf8ff4a44fc0337d5e8f28071cb5 Mon Sep 17 00:00:00 2001 From: Jennifer Taylor Date: Tue, 25 May 2021 02:01:17 +0000 Subject: [PATCH] Implement AP2_IMAGE tag support into renderer. --- bemani/format/afp/render.py | 183 +++++++++++++++++++++++++++++++----- 1 file changed, 157 insertions(+), 26 deletions(-) diff --git a/bemani/format/afp/render.py b/bemani/format/afp/render.py index 1a2850a..74b2647 100644 --- a/bemani/format/afp/render.py +++ b/bemani/format/afp/render.py @@ -2,7 +2,20 @@ from typing import Any, Dict, Generator, List, Set, Tuple, Optional, Union from PIL import Image # type: ignore from .blend import affine_composite -from .swf import SWF, Frame, Tag, AP2ShapeTag, AP2DefineSpriteTag, AP2PlaceObjectTag, AP2RemoveObjectTag, AP2DoActionTag, AP2DefineFontTag, AP2DefineEditTextTag, AP2PlaceCameraTag +from .swf import ( + SWF, + Frame, + Tag, + AP2ShapeTag, + AP2DefineSpriteTag, + AP2PlaceObjectTag, + AP2RemoveObjectTag, + AP2DoActionTag, + AP2DefineFontTag, + AP2DefineEditTextTag, + AP2PlaceCameraTag, + AP2ImageTag, +) from .decompile import ByteCode from .types import Color, Matrix, Point, Rectangle, AP2Trigger, AP2Action, PushAction, StoreRegisterAction, StringConstant, Register, NULL, UNDEFINED, GLOBAL, ROOT, PARENT, THIS, CLIP from .geo import Shape, DrawParams @@ -14,10 +27,11 @@ class RegisteredClip: # SWF as well as AP2DefineSpriteTags which are essentially embedded movie clips. The # tag_id is the AP2DefineSpriteTag that created us, or None if this is the clip for # the root of the movie. - def __init__(self, tag_id: Optional[int], frames: List[Frame], tags: List[Tag]) -> None: + 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})" @@ -37,6 +51,16 @@ class RegisteredShape: return f"RegisteredShape(tag_id={self.tag_id}, vertex_points={self.vertex_points}, tex_points={self.tex_points}, tex_colors={self.tex_colors}, draw_params={self.draw_params})" +class 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: @@ -65,7 +89,7 @@ class PlacedObject: self.mask = mask @property - def source(self) -> Union[RegisteredClip, RegisteredShape, RegisteredDummy]: + def source(self) -> Union[RegisteredClip, RegisteredShape, RegisteredImage, RegisteredDummy]: raise NotImplementedError("Only implemented in subclass!") @property @@ -151,27 +175,33 @@ class PlacedClip(PlacedObject): 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})" + 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: - 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 gotoAndStop function!") + actual_frame = self.__resolve_frame(frame) + if actual_frame is None: + print(f"WARNING: Unrecognized frame {frame} to gotoAndStop function!") return - if frame <= 0 or frame > len(self.source.frames): + if actual_frame <= 0 or actual_frame > len(self.source.frames): return - self.requested_frame = frame + self.requested_frame = actual_frame self.playing = False 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. + actual_frame = self.__resolve_frame(frame) + if actual_frame is None: print(f"WARNING: Non-integer frame {frame} to gotoAndPlay function!") return - if frame <= 0 or frame > len(self.source.frames): + if actual_frame <= 0 or actual_frame > len(self.source.frames): return - self.requested_frame = frame + self.requested_frame = actual_frame self.playing = True def stop(self) -> None: @@ -196,6 +226,32 @@ class PlacedClip(PlacedObject): self.requested_frame = val + 1 +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_offset: Point, + transform: Matrix, + mult_color: Color, + add_color: Color, + blend: int, + mask: Optional[Mask], + source: RegisteredImage, + ) -> None: + super().__init__(object_id, depth, rotation_offset, transform, mult_color, add_color, 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__( @@ -275,6 +331,18 @@ class AEPLib: # I have no idea what this should do, so let's ignore it. pass + 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 + class ASDLib: def sound_play(self, sound: Any) -> None: @@ -299,7 +367,7 @@ class AFPRenderer(VerboseOutput): self.swfs: Dict[str, SWF] = swfs # Internal render parameters. - self.__registered_objects: Dict[int, Union[RegisteredShape, RegisteredClip, RegisteredDummy]] = {} + self.__registered_objects: Dict[int, Union[RegisteredShape, RegisteredClip, RegisteredImage, RegisteredDummy]] = {} self.__root: Optional[PlacedClip] = None # List of imports that we provide stub implementations for. @@ -528,8 +596,6 @@ class AFPRenderer(VerboseOutput): if tag.reference not in self.shapes: raise Exception(f"Cannot find shape reference {tag.reference}!") - if tag.id in self.__registered_objects: - raise Exception(f"Cannot register {tag.reference} as object slot {tag.id} is already taken!") self.__registered_objects[tag.id] = RegisteredShape( tag.id, @@ -542,14 +608,25 @@ class AFPRenderer(VerboseOutput): # Didn't place a new clip, didn't change anything. return None, False + elif isinstance(tag, AP2ImageTag): + self.vprint(f"{prefix} Loading {tag.reference} into object slot {tag.id}") + + if tag.reference not in self.textures: + raise Exception(f"Cannot find texture reference {tag.reference}!") + + self.__registered_objects[tag.id] = RegisteredImage( + tag.id, + tag.reference, + ) + + # Didn't place a new clip, didn't change anything. + return None, False + elif isinstance(tag, AP2DefineSpriteTag): self.vprint(f"{prefix} Loading Sprite into object slot {tag.id}") - if tag.id in self.__registered_objects: - raise Exception(f"Cannot register sprite as object slot {tag.id} is already taken!") - # Register a new clip that we might reference to execute. - self.__registered_objects[tag.id] = RegisteredClip(tag.id, tag.frames, tag.tags) + 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 @@ -584,6 +661,21 @@ class AFPRenderer(VerboseOutput): 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_offset, + new_transform, + new_mult_color, + new_add_color, + new_blend, + obj.mask, + newobj, + ) + # Didn't place a new clip, changed the parent clip. return None, True elif isinstance(newobj, RegisteredClip): @@ -655,6 +747,23 @@ class AFPRenderer(VerboseOutput): ) ) + # 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_offset or Point.identity(), + tag.transform or Matrix.identity(), + tag.mult_color or Color(1.0, 1.0, 1.0, 1.0), + tag.add_color or Color(0.0, 0.0, 0.0, 0.0), + tag.blend or 0, + None, + newobj, + ) + ) + # Didn't place a new clip, changed the parent clip. return None, True elif isinstance(newobj, RegisteredClip): @@ -916,6 +1025,14 @@ class AFPRenderer(VerboseOutput): if texture is not None: img = affine_composite(img, add_color, mult_color, transform, mask, blend, texture, single_threaded=self.__single_threaded) + elif isinstance(renderable, PlacedImage): + if only_depths is not None and renderable.depth not in only_depths: + # Not on the correct depth plane. + return img + + # This is a shape draw reference. + texture = self.textures[renderable.source.reference] + img = affine_composite(img, add_color, mult_color, transform, mask, blend, texture, single_threaded=self.__single_threaded) elif isinstance(renderable, PlacedDummy): # Nothing to do! pass @@ -1016,8 +1133,8 @@ class AFPRenderer(VerboseOutput): # Return if anything was modified. return changed - def __handle_imports(self, swf: SWF) -> Dict[int, Union[RegisteredShape, RegisteredClip, RegisteredDummy]]: - external_objects: Dict[int, Union[RegisteredShape, RegisteredClip, RegisteredDummy]] = {} + 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(): @@ -1044,7 +1161,7 @@ class AFPRenderer(VerboseOutput): # Return our newly populated registered object table containing all imports! return external_objects - def __find_import(self, swf: SWF, tag_id: int) -> Union[RegisteredShape, RegisteredClip, RegisteredDummy]: + 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: @@ -1056,6 +1173,7 @@ class AFPRenderer(VerboseOutput): None, swf.frames, swf.tags, + swf.labels, ) tag = self.__find_tag(root_clip, tag_id) @@ -1064,7 +1182,7 @@ class AFPRenderer(VerboseOutput): return RegisteredDummy(tag_id) return tag - def __find_tag(self, clip: RegisteredClip, tag_id: int) -> Optional[Union[RegisteredShape, RegisteredClip, RegisteredDummy]]: + 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)] @@ -1086,8 +1204,20 @@ class AFPRenderer(VerboseOutput): 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) + 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. @@ -1132,6 +1262,7 @@ class AFPRenderer(VerboseOutput): None, swf.frames, swf.tags, + swf.labels, ), ) self.__root = root_clip