1
0
mirror of synced 2024-11-14 18:07:36 +01:00
bemaniutils/bemani/format/afp/geo.py

281 lines
11 KiB
Python

import os
import struct
from typing import Any, Dict, List, Optional
from .types import Color, Point, Rectangle
from .util import descramble_text
class Shape:
def __init__(
self,
name: str,
data: bytes,
) -> None:
self.name = name
self.data = data
# Vertex points outlining this shape. These are in pixels and the rectangle they outline
# should match the size of the texture in pixels.
self.vertex_points: List[Point] = []
# Texture points, as used alongside vertex chunks when the shape contains a texture. These
# are in floating points that when multiplied by the width and height of the original
# texture sheet that the texture was taken from (multiplied by two), should match the uvrect
# of the texture exactly.
self.tex_points: List[Point] = []
# Colors for texture points, if they exist in the file.
self.tex_colors: List[Color] = []
# Actual shape drawing parameters.
self.draw_params: List[DrawParams] = []
# Bounds of the shape.
self.bounds: Rectangle = Rectangle.Empty()
# Whether this is parsed.
self.parsed = False
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
return {
'name': self.name,
'vertex_points': [p.as_dict(*args, **kwargs) for p in self.vertex_points],
'tex_points': [p.as_dict(*args, **kwargs) for p in self.tex_points],
'tex_colors': [c.as_dict(*args, **kwargs) for c in self.tex_colors],
'draw_params': [d.as_dict(*args, **kwargs) for d in self.draw_params],
'bounds': self.bounds.as_dict(args, kwargs),
}
def __repr__(self) -> str:
return os.linesep.join([
*[f"vertex point: {vertex}" for vertex in self.vertex_points],
*[f"tex point: {tex}" for tex in self.tex_points],
*[f"tex color: {color}" for color in self.tex_colors],
*[f"draw params: {params}" for params in self.draw_params],
*[f"bounds: {self.bounds}"],
])
def get_until_null(self, offset: int) -> bytes:
out = b""
while self.data[offset] != 0:
out += self.data[offset:(offset + 1)]
offset += 1
return out
def parse(self, text_obfuscated: bool = True) -> None:
# First, grab the header bytes.
magic = self.data[0:4]
if magic == b"D2EG":
endian = "<"
elif magic == b"GE2D":
endian = ">"
else:
raise Exception("Invalid magic value in GE2D structure!")
# There are two integers at 0x4 and 0x8 which are basically file versions.
filesize, fileflags = struct.unpack(f"{endian}II", self.data[12:20])
if filesize != len(self.data):
raise Exception("Unexpected file size for GE2D structure!")
geo_has_bounds_floats = bool(fileflags & 0x4)
vertex_well_formed_rectangle = bool(fileflags & 0x8)
if fileflags & ~0xC:
raise Exception(f"Unexpected file flags {fileflags} in GE2D structure!")
vertex_count, tex_count, color_count, label_count, render_params_count, _ = struct.unpack(
f"{endian}HHHHHH",
self.data[20:32],
)
vertex_offset, tex_offset, color_offset, label_offset, render_params_offset = struct.unpack(
f"{endian}IIIII",
self.data[32:52],
)
vertex_points: List[Point] = []
if vertex_offset != 0:
# If fileflags & 0x8 is set, these go in clockwise order starting at the top left
# point and there are only 8 of them, making a rectangle.
for vertexno in range(vertex_count):
vertexno_offset = vertex_offset + (8 * vertexno)
x, y = struct.unpack(f"{endian}ff", self.data[vertexno_offset:vertexno_offset + 8])
vertex_points.append(Point(x, y))
self.vertex_points = vertex_points
tex_points: List[Point] = []
if tex_offset != 0:
for texno in range(tex_count):
texno_offset = tex_offset + (8 * texno)
x, y = struct.unpack(f"{endian}ff", self.data[texno_offset:texno_offset + 8])
tex_points.append(Point(x, y))
self.tex_points = tex_points
colors: List[Color] = []
if color_offset != 0:
for colorno in range(color_count):
colorno_offset = color_offset + (4 * colorno)
rgba = struct.unpack(f"{endian}I", self.data[colorno_offset:colorno_offset + 4])[0]
color = Color(
a=(rgba & 0xFF) / 255.0,
b=((rgba >> 8) & 0xFF) / 255.0,
g=((rgba >> 16) & 0xFF) / 255.0,
r=((rgba >> 24) & 0xFF) / 255.0,
)
colors.append(color)
self.tex_colors = colors
labels: List[str] = []
if label_offset != 0:
for labelno in range(label_count):
labelno_offset = label_offset + (4 * labelno)
labelptr = struct.unpack(f"{endian}I", self.data[labelno_offset:labelno_offset + 4])[0]
bytedata = self.get_until_null(labelptr)
labels.append(descramble_text(bytedata, text_obfuscated))
# Parse rectangle bounds.
if geo_has_bounds_floats:
left, right, top, bottom = struct.unpack(f"{endian}ffff", self.data[52:68])
self.bounds = Rectangle(
left=left,
right=right,
top=top,
bottom=bottom,
)
elif vertex_well_formed_rectangle:
self.bounds = Rectangle(
left=vertex_points[0].x,
right=vertex_points[1].x,
top=vertex_points[0].y,
bottom=vertex_points[2].y,
)
else:
if vertex_points:
self.bounds.left = vertex_points[0].x
self.bounds.right = vertex_points[0].x
self.bounds.top = vertex_points[0].y
self.bounds.bottom = vertex_points[0].y
for pt in vertex_points:
if pt.x < self.bounds.left:
self.bounds.left = pt.x
if pt.x > self.bounds.right:
self.bounds.right = pt.x
if pt.y < self.bounds.top:
self.bounds.top = pt.y
if pt.y > self.bounds.bottom:
self.bounds.bottom = pt.y
draw_params: List[DrawParams] = []
if render_params_offset != 0:
# The actual render parameters for the shape. This dictates how the texture values
# are used when drawing shapes, whether to use a blend value or draw a primitive, etc.
for render_paramsno in range(render_params_count):
render_paramsno_offset = render_params_offset + (16 * render_paramsno)
mode, flags, tex1, tex2, trianglecount, unk, rgba, triangleoffset = struct.unpack(
f"{endian}BBBBHHII",
self.data[(render_paramsno_offset):(render_paramsno_offset + 16)]
)
if mode != 4:
raise Exception("Unexpected mode in GE2D structure!")
if (flags & 0x2) and len(labels) == 0:
raise Exception("GE2D structure has a texture, but no region labels present!")
if (flags & 0x2) and (tex1 == 0xFF):
raise Exception("GE2D structure requests a texture, but no texture pointer present!")
if tex2 != 0xFF:
raise Exception("GE2D structure requests a second texture, but we don't support this!")
if unk != 0x0:
raise Exception("Unhandled unknown data in GE2D structure!")
color = Color(
a=(rgba & 0xFF) / 255.0,
b=((rgba >> 8) & 0xFF) / 255.0,
g=((rgba >> 16) & 0xFF) / 255.0,
r=((rgba >> 24) & 0xFF) / 255.0,
)
verticies: List[int] = []
for render_paramstriangleno in range(trianglecount):
render_paramstriangleno_offset = triangleoffset + (2 * render_paramstriangleno)
tex_offset = struct.unpack(f"{endian}H", self.data[render_paramstriangleno_offset:(render_paramstriangleno_offset + 2)])[0]
verticies.append(tex_offset)
# Seen bits are 0x1, 0x2, 0x4, 0x8 so far.
# 0x1 Is a "this shape is instantiable/drawable" bit.
# 0x2 Is the shape having a texture.
# 0x4 Is the shape having a texture color per texture point.
# 0x8 Is "draw background color/blend" flag.
# 0x40 Is a "normalize texture coordinates" flag. It performs the below algorithm.
if (flags & (0x2 | 0x40)) == (0x2 | 0x40):
# The tex offsets point at the tex vals parsed above, and are used in conjunction with
# texture/region metrics to calcuate some offsets. First, the region left/right/top/bottom
# is divided by 2 (looks like a scaling of 2 for regions to textures is hardcoded) and then
# divided by the texture width/height (as relevant). The returned metrics are in texture space
# where 0.0 is the origin and 1.0 is the furthest right/down. The metrics are then multiplied
# by the texture point pairs that appear above, meaning they should be treated as percentages.
pass
draw_params.append(
DrawParams(
flags=flags,
region=labels[tex1] if (flags & 0x2) else None,
vertexes=verticies if (flags & 0x6) else [],
blend=color if (flags & 0x8) else None,
)
)
self.draw_params = draw_params
self.parsed = True
class DrawParams:
def __init__(
self,
flags: int,
region: Optional[str] = None,
vertexes: List[int] = [],
blend: Optional[Color] = None,
) -> None:
self.flags = flags
self.region = region
self.vertexes = vertexes
self.blend = blend
def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
return {
'flags': self.flags,
'region': self.region,
'vertexes': self.vertexes,
'blend': self.blend.as_dict(*args, **kwargs) if self.blend else None,
}
def __repr__(self) -> str:
flagbits: List[str] = []
if self.flags & 0x1:
flagbits.append("(Instantiable)")
if self.flags & 0x2:
flagbits.append("(Includes Texture)")
if self.flags & 0x4:
flagbits.append("(Includes Texture Color)")
if self.flags & 0x8:
flagbits.append("(Includes Blend Color)")
if self.flags & 0x40:
flagbits.append("(Needs Tex Point Normalization)")
flagspart = f"flags: {hex(self.flags)} {' '.join(flagbits)}"
if self.flags & 0x2:
texpart = f", region: {self.region}, vertexes: {', '.join(str(x) for x in self.vertexes)}"
else:
texpart = ""
if self.flags & 0x8:
blendpart = f", blend: {self.blend}"
else:
blendpart = ""
return f"{flagspart}{texpart}{blendpart}"