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
|
2024-12-19 23:35:14 -03:00
|
|
|
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,
|
2024-11-12 13:51:49 -03:00
|
|
|
"monitor_image": ObjectId.MONITOR_IMAGE,
|
2024-11-12 19:34:17 -03:00
|
|
|
"shield": ObjectId.SHIELD,
|
2024-12-19 23:35:14 -03:00
|
|
|
"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
|
|
|
|
|
|
|
|
2024-12-19 23:35:14 -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))
|
2024-09-30 23:59:11 -03:00
|
|
|
f.write(c_ubyte(flipmask))
|
2024-10-02 02:35:15 -03:00
|
|
|
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)
|