1
0
mirror of synced 2024-11-15 10:27:41 +01:00
bemaniutils/bemani/format/afp/render.py

267 lines
11 KiB
Python

from typing import Any, Dict, List, Tuple, Optional
from PIL import Image # type: ignore
from .swf import SWF, Frame, Tag, AP2ShapeTag, AP2DefineSpriteTag, AP2PlaceObjectTag, AP2RemoveObjectTag
from .types import Color, Matrix, Point
from .geo import Shape
from .util import VerboseOutput
class Clip:
def __init__(self, tag_id: Optional[int], frames: List[Frame], tags: List[Tag]) -> None:
self.tag_id = tag_id
self.frames = frames
self.tags = tags
self.frameno = 0
def frame(self) -> Frame:
return self.frames[self.frameno]
def advance(self) -> None:
if not self.finished():
self.frameno += 1
def finished(self) -> bool:
return self.frameno == len(self.frames)
def running(self) -> bool:
return not self.finished()
class PlacedObject:
def __init__(self, parent_sprite: Optional[int], tag: AP2PlaceObjectTag) -> None:
self.parent_sprite = parent_sprite
self.tag = tag
class AFPRenderer(VerboseOutput):
def __init__(self, shapes: Dict[str, Shape] = {}, textures: Dict[str, Any] = {}, swfs: Dict[str, SWF] = {}) -> None:
super().__init__()
self.shapes: Dict[str, Shape] = shapes
self.textures: Dict[str, Any] = textures
self.swfs: Dict[str, SWF] = swfs
# Internal render parameters
self.__visible_tag: Optional[int] = None
self.__ided_tags: Dict[int, Tag] = {}
self.__registered_shapes: Dict[int, Shape] = {}
self.__placed_objects: List[PlacedObject] = []
def add_shape(self, name: str, data: Shape) -> None:
if not data.parsed:
data.parse()
self.shapes[name] = data
def add_texture(self, name: str, data: Any) -> None:
self.textures[name] = data
def add_swf(self, name: str, data: SWF) -> None:
if not data.parsed:
data.parse()
self.swfs[name] = data
def render_path(self, path: str, verbose: bool = False) -> Tuple[int, List[Any]]:
components = path.split(".")
if len(components) > 2:
raise Exception('Expected a path in the form of "moviename" or "moviename.exportedtag"!')
for name, swf in self.swfs.items():
if swf.exported_name == components[0]:
# This is the SWF we care about.
with self.debugging(verbose):
return self.__render(swf, components[1] if len(components) > 1 else None)
raise Exception(f'{path} not found in registered SWFs!')
def __place(self, tag: Tag, parent_sprite: Optional[int], prefix: str = "") -> List[Clip]:
if isinstance(tag, AP2ShapeTag):
self.vprint(f"{prefix} Loading {tag.reference} into shape slot {tag.id}")
if tag.reference not in self.shapes:
raise Exception(f"Cannot find shape reference {tag.reference}!")
self.__registered_shapes[tag.id] = self.shapes[tag.reference]
return []
elif isinstance(tag, AP2DefineSpriteTag):
self.vprint(f"{prefix} Registering Sprite Tag {tag.id}")
# Register a new clip that we have to execute.
clip = Clip(tag.id, tag.frames, tag.tags)
clips: List[Clip] = [clip]
# Now, we need to run the first frame of this clip, since that's this frame.
if clip.running():
frame = clip.frame()
if frame.num_tags > 0:
self.vprint(f"{prefix} First Frame Initialization, Start Frame: {frame.start_tag_offset}, Num Frames: {frame.num_tags}")
for child in clip.tags[frame.start_tag_offset:(frame.start_tag_offset + frame.num_tags)]:
clips.extend(self.__place(child, parent_sprite=tag.id, prefix=" "))
# Finally, return the new clips we registered.
return clips
elif isinstance(tag, AP2PlaceObjectTag):
if tag.update:
raise Exception("Don't support update tags yet!")
else:
self.vprint(f"{prefix} Placing Object ID {tag.object_id} onto Depth {tag.depth}")
self.__placed_objects.append(PlacedObject(parent_sprite, tag))
# TODO: Handle triggers for this object.
return []
elif isinstance(tag, AP2RemoveObjectTag):
self.vprint(f"{prefix} Removing Object ID {tag.object_id} from Depth {tag.depth}")
if tag.object_id != 0:
# Remove the identified object by object ID and depth.
self.__placed_objects = [
o for o in self.__placed_objects
if o.tag.object_id == tag.object_id and o.tag.depth == tag.depth
]
else:
# Remove the last placed object at this depth.
for i in range(len(self.__placed_objects)):
real_index = len(self.__placed_objects) - (i + 1)
if self.__placed_objects[real_index].tag.depth == tag.depth:
self.__placed_objects = self.__placed_objects[:real_index] + self.__placed_objects[(real_index + 1):]
break
return []
else:
raise Exception(f"Failed to process tag: {tag}")
def __render_object(self, img: Any, tag: AP2PlaceObjectTag) -> Any:
if tag.source_tag_id is None:
self.vprint(" Nothing to render!")
return img
# Double check supported options.
if tag.mult_color or tag.add_color:
raise Exception("Don't support color blending yet!")
# Look up the affine transformation matrix and rotation/origin.
transform = tag.transform or Matrix.identity()
origin = tag.rotation_offset or Point.identity()
# Look up source shape.
if tag.source_tag_id not in self.__registered_shapes:
# TODO: Lots of animations are referencing other sprite tags with transform
# offsets and such. We need to support this. However, I'm not sure how the
# original gets hidden...
raise Exception(f"Failed to find shape tag {tag.source_tag_id} for object render!")
shape = self.__registered_shapes[tag.source_tag_id]
for params in shape.draw_params:
if not (params.flags & 0x1):
# Not instantiable, don't render.
return img
if params.flags & 0x4 or params.flags & 0x8:
raise Exception("Don't support shape blend or uv coordinate color yet!")
texture = None
if params.flags & 0x2:
# We need to look up the texture for this.
if params.region not in self.textures:
raise Exception(f"Cannot find texture reference {params.region}!")
texture = self.textures[params.region]
# TODO: Need to do actual affine transformations here.
offset = transform.multiply_point(Point.identity().subtract(origin))
# Now, render out the texture.
cutoff = Point.identity()
if offset.x < 0:
cutoff.x = -offset.x
offset.x = 0
if offset.y < 0:
cutoff.y = -offset.y
offset.y = 0
img.alpha_composite(texture, offset.as_tuple(), cutoff.as_tuple())
return img
def __render(self, swf: SWF, export_tag: Optional[str]) -> Tuple[int, List[Any]]:
# If we are rendering only an exported tag, we want to perform the actions of the
# rest of the SWF but not update any layers as a result.
self.__visible_tag = None
if export_tag is not None:
# Make sure this tag is actually present in the SWF.
if export_tag not in swf.exported_tags:
raise Exception(f'{export_tag} is not exported by {swf.exported_name}!')
self.__visible_tag = swf.exported_tags[export_tag]
# Now, we need to make an index of each ID'd tag.
self.__ided_tags = {}
def get_children(tag: Tag) -> List[Tag]:
children: List[Tag] = []
for child in tag.children():
children.extend(get_children(child))
children.append(tag)
return children
all_children: List[Tag] = []
for tag in swf.tags:
all_children.extend(get_children(tag))
for child in all_children:
if child.id is not None:
if child.id in self.__ided_tags:
raise Exception(f"Already have a Tag ID {child.id}!")
self.__ided_tags[child.id] = child
# TODO: Now, we have to resolve imports.
pass
# Now, let's go through each frame, performing actions as necessary.
spf = 1.0 / swf.fps
frames: List[Any] = []
frameno: int = 0
clips: List[Clip] = [Clip(None, swf.frames, swf.tags)]
# Reset any registered shapes.
self.__registered_shapes = {}
while any(c.running() for c in clips):
# Create a new image to render into.
time = spf * float(frameno)
color = swf.color or Color(0.0, 0.0, 0.0, 0.0)
curimage = Image.new("RGBA", (swf.location.width, swf.location.height), color=color.as_tuple())
self.vprint(f"Rendering Frame {frameno} ({time}s)")
# Go through all registered clips, place all needed tags.
newclips: List[Clip] = []
for clip in clips:
if clip.finished():
continue
frame = clip.frame()
if frame.num_tags > 0:
self.vprint(f" Sprite Tag ID: {clip.tag_id}, Start Frame: {frame.start_tag_offset}, Num Frames: {frame.num_tags}")
for tag in clip.tags[frame.start_tag_offset:(frame.start_tag_offset + frame.num_tags)]:
newclips.extend(self.__place(tag, parent_sprite=clip.tag_id))
# Add any new clips that we should process next frame.
clips.extend(newclips)
# Now, render out the placed objects.
for obj in sorted(self.__placed_objects, key=lambda o: o.tag.depth):
if self.__visible_tag != obj.parent_sprite:
continue
self.vprint(f" Rendering placed object ID {obj.tag.object_id} from sprite {obj.parent_sprite} onto Depth {obj.tag.depth}")
curimage = self.__render_object(curimage, obj.tag)
# Advance all the clips and frame now that we processed and rendered them.
for clip in clips:
clip.advance()
frames.append(curimage)
frameno += 1
return int(spf * 1000.0), frames