diff --git a/assets/levels/R2/Z1.OMP b/assets/levels/R2/Z1.OMP new file mode 100644 index 0000000..9e9cbad Binary files /dev/null and b/assets/levels/R2/Z1.OMP differ diff --git a/tools/cookobj/cookobj.py b/tools/cookobj/cookobj.py index e97e178..49d44ba 100755 --- a/tools/cookobj/cookobj.py +++ b/tools/cookobj/cookobj.py @@ -9,9 +9,9 @@ from pprint import pp def main(): map_src = realpath(sys.argv[1]) obj_defs, obj_places = parse_map(map_src) - # pp(obj_places) for key, d in obj_defs.items(): d.write() + obj_places.write() if __name__ == "__main__": diff --git a/tools/cookobj/datatypes.py b/tools/cookobj/datatypes.py index ba16d8b..b148599 100644 --- a/tools/cookobj/datatypes.py +++ b/tools/cookobj/datatypes.py @@ -13,7 +13,7 @@ class DummyObjectId(Enum): RING_3V = -2 @staticmethod - def get(name: str) -> int: + def get(name): switch = { "ring_3h": DummyObjectId.RING_3H, "ring_3v": DummyObjectId.RING_3V, @@ -23,6 +23,37 @@ class DummyObjectId(Enum): return result +class ObjectId(Enum): + RING = 0 + MONITOR = 1 + SPIKES = 2 + CHECKPOINT = 3 + SPRING_YELLOW = 4 + SPRING_RED = 5 + SPRING_YELLOW_DIAGONAL = 6 + SPRING_RED_DIAGONAL = 7 + GOAL_SIGN = 8 + SWITCH = 9 + + @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, + } + result = switch.get(name.lower()) + assert result is not None, f"Unknown common object {name}" + return result + + # OBJECT TABLE DEFINITION (.OTN) LAYOUT # - is_level_specific (u8) # - num_classes (u16) @@ -148,27 +179,45 @@ class ObjectMap: return None -# OBJECT MAP PLACEMENT (.OMP) LAYOUT -# - is_level_specific (u8) -# - Type / ID (s8) -# - Flip Mask (u8) -# - has_properties (u8) -# - vx (s32) -# - vy (s32) -# - Properties (exists depending on Type) -# * Properties layout for monitor (id = 1): -# - kind (u8) +# ======================================= + + +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 @dataclass class MonitorProperties: - kind: str = "" + kind: int = 0 + + def write_to(self, f): + f.write(c_ubyte(self.kind)) ObjectProperties = MonitorProperties | None -# Root for the .OMP datatype @dataclass class ObjectPlacement: is_level_specific: bool = False @@ -194,4 +243,37 @@ class ObjectPlacement: f.write(c_int(self.x)) f.write(c_int(self.y)) f.write(c_ubyte(flipmask)) - # TODO: Properties + 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) +# - has_properties (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: + description = DummyObjectId(p.otype) if p.otype < 0 else ObjectId(p.otype) + print(f"Writing placement for object ID {p.otype} ({description})...") + p.write_to(f) + + def write(self): + with open(self.out, "wb") as f: + self.write_to(f) diff --git a/tools/cookobj/parsers.py b/tools/cookobj/parsers.py index f94c9e6..25f3230 100644 --- a/tools/cookobj/parsers.py +++ b/tools/cookobj/parsers.py @@ -36,6 +36,8 @@ def parse_tileset(firstgid: int, set_src: str) -> (ObjectMap, str): ts = ts.find("tileset") + o.is_level_specific = ts["name"] != "objects_common" + tiles = ts.find_all("tile") o.num_objs = int(ts["tilecount"]) # "classes" becomes an entry on dict o.object_types. @@ -87,7 +89,9 @@ def parse_tileset(firstgid: int, set_src: str) -> (ObjectMap, str): # ] # collision["points"] = points - o.obj_mapping[gid] = gid - firstgid + o.obj_mapping[gid] = ( + (gid - firstgid) if o.is_level_specific else ObjectId.get(od.name).value + ) # Append TOML data if extra: @@ -116,7 +120,6 @@ def parse_tileset(firstgid: int, set_src: str) -> (ObjectMap, str): o.firstgid = firstgid o.out = splitext(set_src)[0] + ".OTD" - o.is_level_specific = ts["name"] != "objects_common" o.num_objs = len(o.object_types) o.name = ts["name"] return (o, ts["name"]) @@ -136,11 +139,9 @@ def parse_object_group( # Get first object's gid. first_obj = sorted(objects, key=lambda x: int(x.get("gid")))[0] first_obj_gid = int(first_obj.get("gid")) - print(f"First object gid: {first_obj_gid}") # Identify if this is from common objects tileset or from level-specific tileset. for key, ts in tilesets.items(): - print(f"First gid: {ts.firstgid}") result = ts.get_is_specific_if_from_this_map(first_obj_gid) if result is not None: current_ts = ts @@ -153,32 +154,38 @@ def parse_object_group( for obj in objects: p = ObjectPlacement() p.is_level_specific = is_level_specific - p.otype = current_ts.get_otype_from_gid(int(obj.get("gid"))) + gid = int(obj.get("gid")) + p.otype = current_ts.get_otype_from_gid(gid) p.x = int(float(obj.get("x"))) p.y = int(float(obj.get("y"))) - p.flipx = False # TODO - p.flipy = False # TODO + p.flipx = bool(gid & (1 << 31)) + p.flipy = bool(gid & (1 << 30)) p.rotcw = int(float(obj.get("rotation", 0))) == 90 p.rotccw = int(float(obj.get("rotation", 0))) == -90 props = obj.find("properties") if props: - # TODO: Check type, build type-specific property, etc + if p.otype == ObjectId.MONITOR.value: + prop = props.find("property") + m = MonitorProperties() + m.kind = MonitorKind.get(prop.get("value")).value + p.properties = m pass - print( - f"Object type {current_ts.object_types[p.otype + current_ts.firstgid].name if p.otype >= 0 else 'DUMMY'}" - ) - pp(p) + # print( + # f"Object type {current_ts.object_types[p.otype + current_ts.firstgid].name if p.otype >= 0 else 'DUMMY'}" + # ) + # pp(p) placements.append(p) return placements -def parse_map(map_src: str) -> (typing.Dict[str, ObjectMap], [ObjectPlacement]): +def parse_map(map_src: str) -> (typing.Dict[str, ObjectMap], ObjectLevelLayout): map = None with open(map_src) as f: map = BeautifulSoup(f, "xml") objmaps = {} - placements = [] + layout = ObjectLevelLayout() + layout.out = realpath(splitext(map_src)[0] + ".OMP") # Get all tilesets that are not 128x128. # Depends on tileset name. @@ -194,6 +201,6 @@ def parse_map(map_src: str) -> (typing.Dict[str, ObjectMap], [ObjectPlacement]): layergroup = map.find(name="group", attrs={"name": "OBJECTS"}) objgroups = layergroup.find_all("objectgroup") for objgroup in objgroups: - placements += parse_object_group(objmaps, objgroup) + layout.placements += parse_object_group(objmaps, objgroup) - return (objmaps, placements) + return (objmaps, layout) diff --git a/tools/object_design.org b/tools/object_design.org index 19c6ce2..ffef41d 100644 --- a/tools/object_design.org +++ b/tools/object_design.org @@ -1701,8 +1701,8 @@ For brevity of detailing, the types will be described as C structs directly. #define OBJ_IDX_CHECKPOINT 3 #define OBJ_IDX_SPRING_Y 4 #define OBJ_IDX_SPRING_R 5 -#define OBJ_IDX_SPRING_D_Y 6 -#define OBJ_IDX_SPRING_D_R 7 +#define OBJ_IDX_SPRING_Y_D 6 +#define OBJ_IDX_SPRING_R_D 7 #define OBJ_IDX_GOAL_SIGN 8 #define OBJ_IDX_SWITCH 9