Begin parsing AFP data into useful structures.
This commit is contained in:
parent
9925b2f6b0
commit
9f6b9eb7d6
@ -1,5 +1,5 @@
|
|||||||
from .geo import Shape, DrawParams
|
from .geo import Shape, DrawParams
|
||||||
from .swf import SWF
|
from .swf import SWF, NamedTagReference
|
||||||
from .container import TXP2File, PMAN, Texture, TextureRegion, Unknown1, Unknown2
|
from .container import TXP2File, PMAN, Texture, TextureRegion, Unknown1, Unknown2
|
||||||
from .types import Matrix, Color, Point, Rectangle, AP2Tag, AP2Action, AP2Object, AP2Pointer, AP2Property
|
from .types import Matrix, Color, Point, Rectangle, AP2Tag, AP2Action, AP2Object, AP2Pointer, AP2Property
|
||||||
|
|
||||||
@ -8,6 +8,7 @@ __all__ = [
|
|||||||
'Shape',
|
'Shape',
|
||||||
'DrawParams',
|
'DrawParams',
|
||||||
'SWF',
|
'SWF',
|
||||||
|
'NamedTagReference',
|
||||||
'TXP2File',
|
'TXP2File',
|
||||||
'PMAN',
|
'PMAN',
|
||||||
'Texture',
|
'Texture',
|
||||||
|
@ -2,13 +2,28 @@ from hashlib import md5
|
|||||||
import os
|
import os
|
||||||
import struct
|
import struct
|
||||||
import sys
|
import sys
|
||||||
from typing import Any, Dict, List, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from .types import Matrix, Color, Point, Rectangle
|
from .types import Matrix, Color, Point, Rectangle
|
||||||
from .types import AP2Action, AP2Tag, AP2Property
|
from .types import AP2Action, AP2Tag, AP2Property
|
||||||
from .util import TrackedCoverage, VerboseOutput, _hex
|
from .util import TrackedCoverage, VerboseOutput, _hex
|
||||||
|
|
||||||
|
|
||||||
|
class NamedTagReference:
|
||||||
|
def __init__(self, swf_name: str, tag_name: str) -> None:
|
||||||
|
self.swf = swf_name
|
||||||
|
self.tag = tag_name
|
||||||
|
|
||||||
|
def as_dict(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
'swf': self.swf,
|
||||||
|
'tag': self.tag,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"{self.swf}.{self.tag}"
|
||||||
|
|
||||||
|
|
||||||
class SWF(TrackedCoverage, VerboseOutput):
|
class SWF(TrackedCoverage, VerboseOutput):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -19,22 +34,47 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||||||
# First, init the coverage engine.
|
# First, init the coverage engine.
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
# Now, initialize parsed data.
|
# Name of this SWF, according to the container it was extracted from.
|
||||||
self.name = name
|
self.name: str = name
|
||||||
self.exported_name = ""
|
|
||||||
self.data = data
|
|
||||||
self.descramble_info = descramble_info
|
|
||||||
|
|
||||||
# Initialize string table. This is used for faster lookup of strings
|
# Name of this SWF, as referenced by other SWFs that require imports from it.
|
||||||
# as well as tracking which strings in the table have been parsed correctly.
|
self.exported_name: str = ""
|
||||||
self.strings: Dict[int, Tuple[str, bool]] = {}
|
|
||||||
|
# Full, unparsed data for this SWF, as well as the descrambling headers.
|
||||||
|
self.data: bytes = data
|
||||||
|
self.descramble_info: bytes = descramble_info
|
||||||
|
|
||||||
|
# Data version of this SWF.
|
||||||
|
self.data_version: int = 0
|
||||||
|
|
||||||
|
# Container version of this SWF.
|
||||||
|
self.container_version: int = 0
|
||||||
|
|
||||||
|
# The requested frames per second this SWF plays at.
|
||||||
|
self.fps: float = 0.0
|
||||||
|
|
||||||
|
# Background color of this SWF movie.
|
||||||
|
self.color: Optional[Color] = None
|
||||||
|
|
||||||
|
# Location of this SWF in screen space.
|
||||||
|
self.location: Rectangle = Rectangle.Empty()
|
||||||
|
|
||||||
|
# Exported tags, indexed by their name and pointing to the Tag ID that name identifies.
|
||||||
|
self.exported_tags: Dict[str, int] = {}
|
||||||
|
|
||||||
|
# Imported tags, indexed by their Tag ID, and pointing at the SWF asset and exported tag name.
|
||||||
|
self.imported_tags: Dict[int, NamedTagReference] = {}
|
||||||
|
|
||||||
|
# SWF string table. This is used for faster lookup of strings as well as
|
||||||
|
# tracking which strings in the table have been parsed correctly.
|
||||||
|
self.__strings: Dict[int, Tuple[str, bool]] = {}
|
||||||
|
|
||||||
def print_coverage(self) -> None:
|
def print_coverage(self) -> None:
|
||||||
# First print uncovered bytes
|
# First print uncovered bytes
|
||||||
super().print_coverage()
|
super().print_coverage()
|
||||||
|
|
||||||
# Now, print uncovered strings
|
# Now, print uncovered strings
|
||||||
for offset, (string, covered) in self.strings.items():
|
for offset, (string, covered) in self.__strings.items():
|
||||||
if covered:
|
if covered:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -752,7 +792,7 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||||||
start_tag_id = frame_info & 0xFFFFF
|
start_tag_id = frame_info & 0xFFFFF
|
||||||
num_tags_to_play = (frame_info >> 20) & 0xFFF
|
num_tags_to_play = (frame_info >> 20) & 0xFFF
|
||||||
|
|
||||||
self.vprint(f"{prefix} Frame Start Tag: {hex(start_tag_id)}, Count: {num_tags_to_play}")
|
self.vprint(f"{prefix} Frame Start Tag: {start_tag_id}, Count: {num_tags_to_play}")
|
||||||
frame_offset += 4
|
frame_offset += 4
|
||||||
|
|
||||||
# Now, parse unknown tags? I have no idea what these are, but they're referencing strings that
|
# Now, parse unknown tags? I have no idea what these are, but they're referencing strings that
|
||||||
@ -814,7 +854,7 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||||||
if byte == 0:
|
if byte == 0:
|
||||||
if curstring:
|
if curstring:
|
||||||
# We found a string!
|
# We found a string!
|
||||||
self.strings[curloc - stringtable_offset] = (bytes(curstring).decode('utf8'), False)
|
self.__strings[curloc - stringtable_offset] = (bytes(curstring).decode('utf8'), False)
|
||||||
curloc = stringtable_offset + i + 1
|
curloc = stringtable_offset + i + 1
|
||||||
curstring = []
|
curstring = []
|
||||||
curloc = stringtable_offset + i + 1
|
curloc = stringtable_offset + i + 1
|
||||||
@ -824,7 +864,7 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||||||
if curstring:
|
if curstring:
|
||||||
raise Exception("Logic error!")
|
raise Exception("Logic error!")
|
||||||
|
|
||||||
if 0 in self.strings:
|
if 0 in self.__strings:
|
||||||
raise Exception("Should not include null string!")
|
raise Exception("Should not include null string!")
|
||||||
|
|
||||||
return bytes(data)
|
return bytes(data)
|
||||||
@ -833,8 +873,8 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||||||
if offset == 0:
|
if offset == 0:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
self.strings[offset] = (self.strings[offset][0], True)
|
self.__strings[offset] = (self.__strings[offset][0], True)
|
||||||
return self.strings[offset][0]
|
return self.__strings[offset][0]
|
||||||
|
|
||||||
def parse(self, verbose: bool = False) -> None:
|
def parse(self, verbose: bool = False) -> None:
|
||||||
with self.covered(len(self.data), verbose):
|
with self.covered(len(self.data), verbose):
|
||||||
@ -847,8 +887,6 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||||||
|
|
||||||
# Start with the basic file header.
|
# Start with the basic file header.
|
||||||
magic, length, version, nameoffset, flags, left, right, top, bottom = struct.unpack("<4sIHHIHHHH", data[0:24])
|
magic, length, version, nameoffset, flags, left, right, top, bottom = struct.unpack("<4sIHHIHHHH", data[0:24])
|
||||||
width = right - left
|
|
||||||
height = bottom - top
|
|
||||||
self.add_coverage(0, 24)
|
self.add_coverage(0, 24)
|
||||||
|
|
||||||
ap2_data_version = magic[0] & 0xFF
|
ap2_data_version = magic[0] & 0xFF
|
||||||
@ -862,24 +900,40 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||||||
if version != 0x200:
|
if version != 0x200:
|
||||||
raise Exception(f"Unsupported AP2 version {version}!")
|
raise Exception(f"Unsupported AP2 version {version}!")
|
||||||
|
|
||||||
|
# The container version is analogous to the SWF file version. I'm pretty sure it
|
||||||
|
# dictates certain things like what properties are available. These appear strictly
|
||||||
|
# additive so we don't concern ourselves with this.
|
||||||
|
self.container_version = ap2_data_version
|
||||||
|
|
||||||
|
# The data version is basically used for how to parse tags. There was an older data
|
||||||
|
# version 0x100 that used more SWF-like bit-packed tags and while lots of code exists
|
||||||
|
# to parse this, the AP2 libraries will reject SWF data with this version.
|
||||||
|
self.data_version = version
|
||||||
|
|
||||||
|
# As far as I can tell, most things only care about the width and height of this
|
||||||
|
# movie, and I think the Shapes are rendered based on the width/height. However, it
|
||||||
|
# can have a non-zero x/y offset and I think this is used when rendering multiple
|
||||||
|
# movie clips?
|
||||||
|
self.location = Rectangle(left=left, right=right, top=top, bottom=bottom)
|
||||||
|
|
||||||
if flags & 0x1:
|
if flags & 0x1:
|
||||||
# This appears to be the animation background color.
|
# This appears to be the animation background color.
|
||||||
rgba = struct.unpack("<I", data[28:32])[0]
|
rgba = struct.unpack("<I", data[28:32])[0]
|
||||||
swf_color = Color(
|
self.color = Color(
|
||||||
r=(rgba & 0xFF) / 255.0,
|
r=(rgba & 0xFF) / 255.0,
|
||||||
g=((rgba >> 8) & 0xFF) / 255.0,
|
g=((rgba >> 8) & 0xFF) / 255.0,
|
||||||
b=((rgba >> 16) & 0xFF) / 255.0,
|
b=((rgba >> 16) & 0xFF) / 255.0,
|
||||||
a=((rgba >> 24) & 0xFF) / 255.0,
|
a=((rgba >> 24) & 0xFF) / 255.0,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
swf_color = None
|
self.color = None
|
||||||
self.add_coverage(28, 4)
|
self.add_coverage(28, 4)
|
||||||
|
|
||||||
if flags & 0x2:
|
if flags & 0x2:
|
||||||
# FPS can be either an integer or a float.
|
# FPS can be either an integer or a float.
|
||||||
fps = struct.unpack("<i", data[24:28])[0] * 0.0009765625
|
self.fps = struct.unpack("<i", data[24:28])[0] * 0.0009765625
|
||||||
else:
|
else:
|
||||||
fps = struct.unpack("<f", data[24:28])[0]
|
self.fps = struct.unpack("<f", data[24:28])[0]
|
||||||
self.add_coverage(24, 4)
|
self.add_coverage(24, 4)
|
||||||
|
|
||||||
if flags & 0x4:
|
if flags & 0x4:
|
||||||
@ -900,14 +954,13 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||||||
|
|
||||||
# Get exported SWF name.
|
# Get exported SWF name.
|
||||||
self.exported_name = self.__get_string(nameoffset)
|
self.exported_name = self.__get_string(nameoffset)
|
||||||
self.add_coverage(nameoffset + stringtable_offset, len(self.exported_name) + 1, unique=False)
|
|
||||||
self.vprint(f"{os.linesep}AFP name: {self.name}")
|
self.vprint(f"{os.linesep}AFP name: {self.name}")
|
||||||
self.vprint(f"Container Version: {hex(ap2_data_version)}")
|
self.vprint(f"Container Version: {hex(self.container_version)}")
|
||||||
self.vprint(f"Version: {hex(version)}")
|
self.vprint(f"Version: {hex(self.data_version)}")
|
||||||
self.vprint(f"Exported Name: {self.exported_name}")
|
self.vprint(f"Exported Name: {self.exported_name}")
|
||||||
self.vprint(f"SWF Flags: {hex(flags)}")
|
self.vprint(f"SWF Flags: {hex(flags)}")
|
||||||
if flags & 0x1:
|
if flags & 0x1:
|
||||||
self.vprint(f" 0x1: Movie background color: {swf_color}")
|
self.vprint(f" 0x1: Movie background color: {self.color}")
|
||||||
else:
|
else:
|
||||||
self.vprint(" 0x2: No movie background color")
|
self.vprint(" 0x2: No movie background color")
|
||||||
if flags & 0x2:
|
if flags & 0x2:
|
||||||
@ -918,8 +971,8 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||||||
self.vprint(" 0x4: Imported tag initializer section present")
|
self.vprint(" 0x4: Imported tag initializer section present")
|
||||||
else:
|
else:
|
||||||
self.vprint(" 0x4: Imported tag initializer section not present")
|
self.vprint(" 0x4: Imported tag initializer section not present")
|
||||||
self.vprint(f"Dimensions: {width}x{height}")
|
self.vprint(f"Dimensions: {self.location.width}x{self.location.height}")
|
||||||
self.vprint(f"Requested FPS: {fps}")
|
self.vprint(f"Requested FPS: {self.fps}")
|
||||||
|
|
||||||
# Exported assets
|
# Exported assets
|
||||||
num_exported_assets = struct.unpack("<H", data[32:34])[0]
|
num_exported_assets = struct.unpack("<H", data[32:34])[0]
|
||||||
@ -928,15 +981,17 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||||||
self.add_coverage(40, 4)
|
self.add_coverage(40, 4)
|
||||||
|
|
||||||
# Parse exported asset tag names and their tag IDs.
|
# Parse exported asset tag names and their tag IDs.
|
||||||
|
self.exported_tags = {}
|
||||||
self.vprint(f"Number of Exported Tags: {num_exported_assets}")
|
self.vprint(f"Number of Exported Tags: {num_exported_assets}")
|
||||||
for assetno in range(num_exported_assets):
|
for assetno in range(num_exported_assets):
|
||||||
asset_data_offset, asset_string_offset = struct.unpack("<HH", data[asset_offset:(asset_offset + 4)])
|
asset_tag_id, asset_string_offset = struct.unpack("<HH", data[asset_offset:(asset_offset + 4)])
|
||||||
self.add_coverage(asset_offset, 4)
|
self.add_coverage(asset_offset, 4)
|
||||||
asset_offset += 4
|
asset_offset += 4
|
||||||
|
|
||||||
asset_name = self.__get_string(asset_string_offset)
|
asset_name = self.__get_string(asset_string_offset)
|
||||||
self.add_coverage(asset_string_offset + stringtable_offset, len(asset_name) + 1, unique=False)
|
self.exported_tags[asset_name] = asset_tag_id
|
||||||
self.vprint(f" {assetno}: Tag Name: {asset_name} Tag ID: {asset_data_offset}")
|
|
||||||
|
self.vprint(f" {assetno}: Tag Name: {asset_name}, Tag ID: {asset_tag_id}")
|
||||||
|
|
||||||
# Tag sections
|
# Tag sections
|
||||||
tags_offset = struct.unpack("<I", data[36:40])[0]
|
tags_offset = struct.unpack("<I", data[36:40])[0]
|
||||||
@ -951,13 +1006,13 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||||||
self.add_coverage(44, 4)
|
self.add_coverage(44, 4)
|
||||||
|
|
||||||
self.vprint(f"Number of Imported Tags: {imported_tags_count}")
|
self.vprint(f"Number of Imported Tags: {imported_tags_count}")
|
||||||
|
self.imported_tags = {}
|
||||||
for i in range(imported_tags_count):
|
for i in range(imported_tags_count):
|
||||||
# First grab the SWF this is importing from, and the number of assets being imported.
|
# First grab the SWF this is importing from, and the number of assets being imported.
|
||||||
swf_name_offset, count = struct.unpack("<HH", data[imported_tags_offset:(imported_tags_offset + 4)])
|
swf_name_offset, count = struct.unpack("<HH", data[imported_tags_offset:(imported_tags_offset + 4)])
|
||||||
self.add_coverage(imported_tags_offset, 4)
|
self.add_coverage(imported_tags_offset, 4)
|
||||||
|
|
||||||
swf_name = self.__get_string(swf_name_offset)
|
swf_name = self.__get_string(swf_name_offset)
|
||||||
self.add_coverage(swf_name_offset + stringtable_offset, len(swf_name) + 1, unique=False)
|
|
||||||
self.vprint(f" Source SWF: {swf_name}")
|
self.vprint(f" Source SWF: {swf_name}")
|
||||||
|
|
||||||
# Now, grab the actual asset names being imported.
|
# Now, grab the actual asset names being imported.
|
||||||
@ -966,7 +1021,8 @@ class SWF(TrackedCoverage, VerboseOutput):
|
|||||||
self.add_coverage(imported_tags_data_offset, 4)
|
self.add_coverage(imported_tags_data_offset, 4)
|
||||||
|
|
||||||
asset_name = self.__get_string(asset_name_offset)
|
asset_name = self.__get_string(asset_name_offset)
|
||||||
self.add_coverage(asset_name_offset + stringtable_offset, len(asset_name) + 1, unique=False)
|
self.imported_tags[asset_id_no] = NamedTagReference(swf_name=swf_name, tag_name=asset_name)
|
||||||
|
|
||||||
self.vprint(f" Tag ID: {asset_id_no}, Requested Asset: {asset_name}")
|
self.vprint(f" Tag ID: {asset_id_no}, Requested Asset: {asset_name}")
|
||||||
|
|
||||||
imported_tags_data_offset += 4
|
imported_tags_data_offset += 4
|
||||||
|
@ -67,5 +67,17 @@ class Rectangle:
|
|||||||
'right': self.right,
|
'right': self.right,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def width(self) -> float:
|
||||||
|
return self.right - self.left
|
||||||
|
|
||||||
|
@property
|
||||||
|
def height(self) -> float:
|
||||||
|
return self.bottom - self.top
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"left: {round(self.left, 5)}, top: {round(self.top, 5)}, bottom: {round(self.bottom, 5)}, right: {round(self.right, 5)}"
|
return f"left: {round(self.left, 5)}, top: {round(self.top, 5)}, bottom: {round(self.bottom, 5)}, right: {round(self.right, 5)}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def Empty() -> "Rectangle":
|
||||||
|
return Rectangle(left=0.0, right=0.0, top=0.0, bottom=0.0)
|
||||||
|
Loading…
Reference in New Issue
Block a user