Initial refactor of COOKOBJ tool

This commit is contained in:
Lucas S. Vieira 2024-09-27 00:35:09 -03:00
parent fa8285268a
commit 7b0ec1a6c9
7 changed files with 277 additions and 0 deletions

Binary file not shown.

1
tools/cookobj/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
__pycache__/

18
tools/cookobj/cookobj.py Executable file
View file

@ -0,0 +1,18 @@
#!/bin/env python
import sys
from parsers import parse_map
from os.path import realpath
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()
if __name__ == "__main__":
main()

131
tools/cookobj/datatypes.py Normal file
View file

@ -0,0 +1,131 @@
from dataclasses import dataclass, field
import typing
from ctypes import c_ubyte, c_byte, c_short, c_ushort, c_int
# from enum import Enum
c_short = c_short.__ctype_be__
c_ushort = c_ushort.__ctype_be__
c_int = c_int.__ctype_be__
# OBJECT TABLE DEFINITION (.OTN) LAYOUT
# - num_classes (u16)
# - Classes of Objects:
# (Note: we don't write the ID since the ID is sequential)
# - num_animations (u16)
# - Animations:
# - num_frames (u16)
# - loopback_frame (u8)
# - 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
def write_to(self, f):
f.write(c_ushort(len(self.frames)))
f.write(c_ubyte(self.loopback))
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))
f.write(c_ubyte(len(self.animations)))
for animation in self.animations:
animation.write_to(f)
MaybeObjectFragment = ObjectFragment | None
@dataclass
class ObjectData:
name: str = ""
animations: [ObjectAnimation] = field(default_factory=list)
fragment: MaybeObjectFragment = None
def write_to(self, f):
f.write(c_ubyte(len(self.animations)))
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:
out: str = ""
firstgid: int = 0
num_objs: int = 0
object_types: typing.Dict[int, ObjectData] = field(default_factory=dict)
def write(self):
with open(self.out, "wb") as f:
self.write_to(f)
def write_to(self, f):
f.write(c_ushort(self.num_objs))
for key, t in self.object_types.items():
t.write_to(f)
# OBJECT MAP PLACEMENT (.OMP) LAYOUT
# - Type / ID (u8)
# - 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 ObjectPlacement:
otype: int = 0
x: int = 0
y: int = 0
flipx: bool = False
flipy: bool = False
rotcw: bool = False
rotct: bool = False

122
tools/cookobj/parsers.py Normal file
View file

@ -0,0 +1,122 @@
from bs4 import BeautifulSoup
import toml
from os.path import realpath, dirname, splitext
from datatypes import *
def _parse_animation(data) -> ObjectAnimation:
anim = ObjectAnimation()
anim.loopback = data["loopback"]
for fr in data["frames"]:
frame = Frame()
fr_get = lambda i: (fr[i] == 1) if i < len(fr) else False
# u0, v0, width, height, flipx, flipy
frame.u0 = fr[0]
frame.v0 = fr[1]
frame.width = fr[2]
frame.height = fr[3]
frame.flipx = fr_get(4)
frame.flipy = fr_get(5)
anim.frames.append(frame)
return anim
def parse_tileset(firstgid: int, set_src: str) -> (ObjectMap, str):
toml_src = splitext(set_src)[0] + ".toml"
ts = None
extra_data = None
o = ObjectMap()
# Load tileset and extra .toml file data
with open(set_src) as f:
ts = BeautifulSoup(f, "xml")
extra_data = toml.load(toml_src)
ts = ts.find("tileset")
tiles = ts.find_all("tile")
o.num_objs = int(ts["tilecount"])
od = ObjectData()
# "classes" becomes an entry on dict o.object_types.
# Emplace "od" there under a proper gid
for i in range(int(ts["tilecount"])):
collision = None
tile = next((x for x in tiles if x["id"] == f"{i}"), None)
if tile:
# Get tile collision
# collisions = tile.find("objectgroup")
# if collisions:
# collisions = collisions.find_all("object", [])
# if collisions[0].get("width"):
# collision = {}
# collision["type"] = "rect"
# collision["x"] = int(collisions[0].get("x"))
# collision["y"] = int(collisions[0].get("y"))
# collision["width"] = int(collisions[0].get("width"))
# collision["height"] = int(collisions[0].get("height"))
# else:
# collision = {}
# collision["type"] = "polygon"
# poly = collisions[0].find("polygon")
# points = poly.get("points").split()
# points = [
# [int(float(p.split(",")[0])), int(float(p.split(",")[1]))]
# for p in points
# ]
# collision["points"] = points
# Get other tile data
od.name = (tile["type"] if tile else "none").lower()
idx = i + firstgid
# Append TOML data
extra = extra_data.get(od.name)
if extra:
animations = extra["animations"]
animations.sort(key=lambda x: x.get("id"))
for data in animations:
od.animations.append(_parse_animation(data))
frag = extra.get("fragment")
if frag:
od.fragment = ObjectFragment()
offset = frag["offset"]
od.fragment.offsetx = offset[0]
od.fragment.offsety = offset[1]
frag_animations = frag["animations"]
frag_animations.sort(key=lambda x: x.get("id"))
for data in animations:
od.fragment.animations.append(_parse_animation(data))
# if collision:
# # TODO: append collision
# pass
o.object_types[idx] = od
else:
o.num_objs -= 1
o.firstgid = firstgid
o.out = splitext(set_src)[0] + ".OTD"
return (o, ts["name"])
def parse_map(map_src: str) -> (typing.Dict[str, ObjectMap], [ObjectPlacement]):
map = None
with open(map_src) as f:
map = BeautifulSoup(f, "xml")
objmaps = {}
placements = []
# Get all tilesets that are not 128x128.
# Depends on tileset name.
# TODO: Perharps use a non-zero firstgid as parameter?
tilesets = [t for t in map.find_all("tileset") if t["source"].find("128") == -1]
for tileset in tilesets:
tileset_src = realpath(dirname(map_src) + "/" + tileset["source"])
loaded_set, ts_name = parse_tileset(int(tileset["firstgid"]), tileset_src)
objmaps[ts_name] = loaded_set
# Retrieve objects placement
return (objmaps, placements)

View file

@ -1,3 +1,5 @@
// -*- mode: c; -*-
#include <type/color.pat>
enum PolygonType: u8 {

3
tools/layouts/otd.hexpat Normal file
View file

@ -0,0 +1,3 @@
// -*- mode: c; -*-
// TODO: Write here the definition for .OTD files!!!