engine-psx/tools/cookobj/datatypes.py

304 lines
8.1 KiB
Python
Raw Normal View History

2024-09-27 00:35:09 -03:00
from dataclasses import dataclass, field
import typing
from ctypes import c_ubyte, c_byte, c_short, c_ushort, c_int
2024-09-30 22:38:31 -03:00
from enum import Enum
2024-09-27 00:35:09 -03:00
c_short = c_short.__ctype_be__
c_ushort = c_ushort.__ctype_be__
c_int = c_int.__ctype_be__
2024-09-30 22:38:31 -03:00
class DummyObjectId(Enum):
RING_3H = -1
RING_3V = -2
2024-10-11 22:12:58 -03:00
STARTPOS = -3
2024-09-30 22:38:31 -03:00
@staticmethod
2024-09-30 23:27:15 -03:00
def get(name):
2024-09-30 22:38:31 -03:00
switch = {
"ring_3h": DummyObjectId.RING_3H,
"ring_3v": DummyObjectId.RING_3V,
2024-10-11 22:12:58 -03:00
"startpos": DummyObjectId.STARTPOS,
2024-09-30 22:38:31 -03:00
}
result = switch.get(name.lower())
assert result is not None, f"Unknown dummy object {name}"
return result
2024-09-30 23:27:15 -03:00
class ObjectId(Enum):
2024-11-12 12:54:10 -03:00
RING = 0x00
MONITOR = 0x01
SPIKES = 0x02
CHECKPOINT = 0x03
SPRING_YELLOW = 0x04
SPRING_RED = 0x05
SPRING_YELLOW_DIAGONAL = 0x06
SPRING_RED_DIAGONAL = 0x07
SWITCH = 0x08
GOAL_SIGN = 0x09
EXPLOSION = 0x0A
MONITOR_IMAGE = 0x0B
SHIELD = 0x0C
BUBBLE_PATCH = 0x0D
BUBBLE = 0x0E
2024-09-30 23:27:15 -03:00
@staticmethod
def get(name):
switch = {
"ring": ObjectId.RING,
"monitor": ObjectId.MONITOR,
"spikes": ObjectId.SPIKES,
"checkpoint": ObjectId.CHECKPOINT,
"spring_yellow": ObjectId.SPRING_YELLOW,
"spring_red": ObjectId.SPRING_RED,
"spring_yellow_diagonal": ObjectId.SPRING_YELLOW_DIAGONAL,
"spring_red_diagonal": ObjectId.SPRING_RED_DIAGONAL,
"goal_sign": ObjectId.GOAL_SIGN,
"switch": ObjectId.SWITCH,
2024-11-12 12:54:10 -03:00
"explosion": ObjectId.EXPLOSION,
"monitor_image": ObjectId.MONITOR_IMAGE,
2024-11-12 19:34:17 -03:00
"shield": ObjectId.SHIELD,
"bubble_patch": ObjectId.BUBBLE_PATCH,
"bubble": ObjectId.BUBBLE,
2024-09-30 23:27:15 -03:00
}
result = switch.get(name.lower())
assert result is not None, f"Unknown common object {name}"
return result
2024-09-27 00:35:09 -03:00
# OBJECT TABLE DEFINITION (.OTN) LAYOUT
2024-09-27 12:45:06 -03:00
# - is_level_specific (u8)
2024-09-27 00:35:09 -03:00
# - num_classes (u16)
# - Classes of Objects:
2024-09-27 12:45:06 -03:00
# - id (u8) {types are sequential but id is used for auto-suficient parsing}
# - has_fragment (u8)
2024-09-27 00:35:09 -03:00
# - num_animations (u16)
# - Animations:
# - num_frames (u16)
2024-09-27 12:45:06 -03:00
# - loopback_frame (s8)
2024-10-09 03:03:27 -03:00
# - duration of a frame (u8)
2024-09-27 00:35:09 -03:00
# - Frames:
# - u0 (u8)
# - v0 (u8)
# - width (u8)
# - height (u8)
# - flipmask (u8)
# - Fragment: (single, exists depending on Type)
# - offsetx (s16)
# - offsety (s16)
# - num_animations (u16)
# - Fragment Animations: (see Animations above)
@dataclass
class Frame:
u0: int = 0
v0: int = 0
width: int = 0
height: int = 0
flipx: bool = False
flipy: bool = False
def write_to(self, f):
flipmask = ((1 << 0) if self.flipx else 0) | ((1 << 1) if self.flipy else 0)
f.write(c_ubyte(self.u0))
f.write(c_ubyte(self.v0))
f.write(c_ubyte(self.width))
f.write(c_ubyte(self.height))
f.write(c_ubyte(flipmask))
@dataclass
class ObjectAnimation:
frames: [Frame] = field(default_factory=list)
loopback: int = 0
2024-10-09 03:03:27 -03:00
duration: int = 0
2024-09-27 00:35:09 -03:00
def write_to(self, f):
f.write(c_ushort(len(self.frames)))
2024-09-27 12:45:06 -03:00
f.write(c_byte(self.loopback))
2024-10-09 03:03:27 -03:00
f.write(c_ubyte(self.duration))
2024-09-27 00:35:09 -03:00
for frame in self.frames:
frame.write_to(f)
@dataclass
class ObjectFragment:
offsetx: int = 0
offsety: int = 0
animations: [ObjectAnimation] = field(default_factory=list)
def write_to(self, f):
f.write(c_short(self.offsetx))
f.write(c_short(self.offsety))
2024-09-27 12:45:06 -03:00
f.write(c_ushort(len(self.animations)))
2024-09-27 00:35:09 -03:00
for animation in self.animations:
animation.write_to(f)
MaybeObjectFragment = ObjectFragment | None
@dataclass
class ObjectData:
2024-09-27 12:45:06 -03:00
id: int = -1
2024-09-30 22:38:31 -03:00
gid: int = -1
2024-09-27 00:35:09 -03:00
name: str = ""
animations: [ObjectAnimation] = field(default_factory=list)
fragment: MaybeObjectFragment = None
def write_to(self, f):
2024-09-30 22:38:31 -03:00
f.write(c_byte(self.id))
2024-09-27 12:45:06 -03:00
f.write(c_ubyte(int(self.fragment is not None)))
f.write(c_ushort(len(self.animations)))
2024-09-27 00:35:09 -03:00
for animation in self.animations:
animation.write_to(f)
if self.fragment:
self.fragment.write_to(f)
# Root for the .OTN data type
@dataclass
class ObjectMap:
2024-09-27 12:45:06 -03:00
is_level_specific: bool = False
2024-09-30 22:38:31 -03:00
name: str = ""
2024-09-27 00:35:09 -03:00
out: str = ""
firstgid: int = 0
num_objs: int = 0
object_types: typing.Dict[int, ObjectData] = field(default_factory=dict)
2024-09-30 22:38:31 -03:00
# Mapping of dummy objects (gid -> actual id)
obj_mapping: typing.Dict[int, int] = field(default_factory=dict)
def get_otype_from_gid(self, gid: int) -> int:
gid = gid & ~(0b1111 << 29)
return self.obj_mapping[gid]
2024-09-27 00:35:09 -03:00
def write(self):
with open(self.out, "wb") as f:
self.write_to(f)
def write_to(self, f):
2024-09-27 12:45:06 -03:00
f.write(c_ubyte(int(self.is_level_specific)))
2024-09-27 00:35:09 -03:00
f.write(c_ushort(self.num_objs))
for key, t in self.object_types.items():
2024-10-01 00:13:59 -03:00
# print(f"Writing object class id {t.id} ({t.name})...")
2024-09-27 00:35:09 -03:00
t.write_to(f)
2024-09-30 22:38:31 -03:00
# I don't have a better name for this. Sorry
def get_is_specific_if_from_this_map(self, dirty_gid: int) -> bool | None:
# Clean GID
gid = dirty_gid & ~(0b1111 << 29)
if gid >= self.firstgid:
return self.is_level_specific
return None
2024-09-27 00:35:09 -03:00
2024-09-30 23:27:15 -03:00
# =======================================
class MonitorKind(Enum):
NONE = 0
RING = 1
SPEEDSHOES = 2
SHIELD = 3
INVINCIBILITY = 4
LIFE = 5
SUPER = 6
@staticmethod
def get(name):
switch = {
"NONE": MonitorKind.NONE,
"RING": MonitorKind.RING,
"SPEEDSHOES": MonitorKind.SPEEDSHOES,
"SHIELD": MonitorKind.SHIELD,
"INVINCIBILITY": MonitorKind.INVINCIBILITY,
"1UP": MonitorKind.LIFE,
"SUPER": MonitorKind.SUPER,
}
result = switch.get(name.upper())
assert result is not None, f"Unknown monitor kind {name}"
return result
2024-09-27 00:35:09 -03:00
2024-09-30 22:38:31 -03:00
@dataclass
class MonitorProperties:
2024-09-30 23:27:15 -03:00
kind: int = 0
def write_to(self, f):
f.write(c_ubyte(self.kind))
2024-09-30 22:38:31 -03:00
@dataclass
class BubblePatchProperties:
frequency: int = 0
def write_to(self, f):
f.write(c_ubyte(self.frequency))
ObjectProperties = MonitorProperties | BubblePatchProperties | None
2024-09-30 22:38:31 -03:00
2024-09-27 00:35:09 -03:00
@dataclass
class ObjectPlacement:
2024-09-30 22:38:31 -03:00
is_level_specific: bool = False
2024-09-27 00:35:09 -03:00
otype: int = 0
x: int = 0
y: int = 0
flipx: bool = False
flipy: bool = False
2024-09-30 22:38:31 -03:00
rotcw: bool = False # clockwise rotation
rotct: bool = False # counterclockwise rotation
properties: ObjectProperties = None
def write_to(self, f):
flipmask = (
((1 << 0) if self.flipx else 0)
| ((1 << 1) if self.flipy else 0)
| ((1 << 2) if self.rotcw else 0)
| ((1 << 3) if self.rotct else 0)
)
f.write(c_ubyte(int(self.is_level_specific)))
f.write(c_byte(self.otype))
f.write(c_ubyte(flipmask))
f.write(c_int(self.x + 32)) # Center X position
f.write(c_int(self.y)) # Already at extreme bottom Y position
2024-09-30 23:27:15 -03:00
if self.properties is not None:
self.properties.write_to(f)
# OBJECT MAP PLACEMENT (.OMP) LAYOUT
# - num_objects (u16)
# - Array of object placements:
# - is_level_specific (u8)
# - Type / ID (s8)
# - Flip Mask (u8)
# - vx (s32)
# - vy (s32)
# - Properties (exists depending on Type)
# * Properties layout for monitor (id = 1):
# - kind (u8)
# Root for the .OMP datatype
@dataclass
class ObjectLevelLayout:
out: str = ""
placements: [ObjectPlacement] = field(default_factory=list)
def write_to(self, f):
f.write(c_ushort(len(self.placements)))
for p in self.placements:
2024-10-01 00:13:59 -03:00
# description = DummyObjectId(p.otype) if p.otype < 0 else ObjectId(p.otype)
# if description == ObjectId.MONITOR:
# description = f"{description}: {MonitorKind(p.properties.kind)}"
# print(f"Placing object at {(p.x, p.y)} => {description}...")
2024-09-30 23:27:15 -03:00
p.write_to(f)
def write(self):
with open(self.out, "wb") as f:
self.write_to(f)