Progress on object placement layout

This commit is contained in:
Lucas S. Vieira 2024-09-30 22:38:31 -03:00
parent 93c4b9fb6c
commit e28ede29a7
5 changed files with 138 additions and 14 deletions

View file

@ -9,7 +9,7 @@ from pprint import pp
def main(): def main():
map_src = realpath(sys.argv[1]) map_src = realpath(sys.argv[1])
obj_defs, obj_places = parse_map(map_src) obj_defs, obj_places = parse_map(map_src)
pp(obj_places) # pp(obj_places)
for key, d in obj_defs.items(): for key, d in obj_defs.items():
d.write() d.write()

View file

@ -1,14 +1,28 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
import typing import typing
from ctypes import c_ubyte, c_byte, c_short, c_ushort, c_int from ctypes import c_ubyte, c_byte, c_short, c_ushort, c_int
from enum import Enum
# from enum import Enum
c_short = c_short.__ctype_be__ c_short = c_short.__ctype_be__
c_ushort = c_ushort.__ctype_be__ c_ushort = c_ushort.__ctype_be__
c_int = c_int.__ctype_be__ c_int = c_int.__ctype_be__
class DummyObjectId(Enum):
RING_3H = -1
RING_3V = -2
@staticmethod
def get(name: str) -> int:
switch = {
"ring_3h": DummyObjectId.RING_3H,
"ring_3v": DummyObjectId.RING_3V,
}
result = switch.get(name.lower())
assert result is not None, f"Unknown dummy object {name}"
return result
# OBJECT TABLE DEFINITION (.OTN) LAYOUT # OBJECT TABLE DEFINITION (.OTN) LAYOUT
# - is_level_specific (u8) # - is_level_specific (u8)
# - num_classes (u16) # - num_classes (u16)
@ -82,12 +96,13 @@ MaybeObjectFragment = ObjectFragment | None
@dataclass @dataclass
class ObjectData: class ObjectData:
id: int = -1 id: int = -1
gid: int = -1
name: str = "" name: str = ""
animations: [ObjectAnimation] = field(default_factory=list) animations: [ObjectAnimation] = field(default_factory=list)
fragment: MaybeObjectFragment = None fragment: MaybeObjectFragment = None
def write_to(self, f): def write_to(self, f):
f.write(c_ubyte(self.id)) f.write(c_byte(self.id))
f.write(c_ubyte(int(self.fragment is not None))) f.write(c_ubyte(int(self.fragment is not None)))
f.write(c_ushort(len(self.animations))) f.write(c_ushort(len(self.animations)))
for animation in self.animations: for animation in self.animations:
@ -100,11 +115,19 @@ class ObjectData:
@dataclass @dataclass
class ObjectMap: class ObjectMap:
is_level_specific: bool = False is_level_specific: bool = False
name: str = ""
out: str = "" out: str = ""
firstgid: int = 0 firstgid: int = 0
num_objs: int = 0 num_objs: int = 0
object_types: typing.Dict[int, ObjectData] = field(default_factory=dict) object_types: typing.Dict[int, ObjectData] = field(default_factory=dict)
# 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]
def write(self): def write(self):
with open(self.out, "wb") as f: with open(self.out, "wb") as f:
self.write_to(f) self.write_to(f)
@ -116,10 +139,20 @@ class ObjectMap:
print(f"Writing object class id {t.id} ({t.name})...") print(f"Writing object class id {t.id} ({t.name})...")
t.write_to(f) t.write_to(f)
# 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
# OBJECT MAP PLACEMENT (.OMP) LAYOUT # OBJECT MAP PLACEMENT (.OMP) LAYOUT
# - Type / ID (u8) # - is_level_specific (u8)
# - Type / ID (s8)
# - Flip Mask (u8) # - Flip Mask (u8)
# - has_properties (u8)
# - vx (s32) # - vx (s32)
# - vy (s32) # - vy (s32)
# - Properties (exists depending on Type) # - Properties (exists depending on Type)
@ -127,13 +160,38 @@ class ObjectMap:
# - kind (u8) # - kind (u8)
@dataclass
class MonitorProperties:
kind: str = ""
ObjectProperties = MonitorProperties | None
# Root for the .OMP datatype # Root for the .OMP datatype
@dataclass @dataclass
class ObjectPlacement: class ObjectPlacement:
is_level_specific: bool = False
otype: int = 0 otype: int = 0
x: int = 0 x: int = 0
y: int = 0 y: int = 0
flipx: bool = False flipx: bool = False
flipy: bool = False flipy: bool = False
rotcw: bool = False rotcw: bool = False # clockwise rotation
rotct: bool = False 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_int(self.x))
f.write(c_int(self.y))
f.write(c_ubyte(flipmask))
# TODO: Properties

View file

@ -1,6 +1,7 @@
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import toml import toml
from os.path import realpath, dirname, splitext from os.path import realpath, dirname, splitext
from pprint import pp
from datatypes import * from datatypes import *
@ -47,14 +48,21 @@ def parse_tileset(firstgid: int, set_src: str) -> (ObjectMap, str):
od = ObjectData() od = ObjectData()
od.id = obj_id od.id = obj_id
od.name = (str(tile["type"]) if tile else "none").lower() od.name = (str(tile["type"]) if tile else "none").lower()
gid = int(tile["id"]) + firstgid
extra = extra_data.get(od.name) extra = extra_data.get(od.name)
# If this is a dummy object (e.g. rows of rings), we don't # If this is a dummy object (e.g. rows of rings), we don't
# need to register it # need to register it
# TODO: These dummy objects will be needed somewhere else!
# We actually need to register them, yes! But somewhere
# else.
if extra and extra.get("dummy", False): if extra and extra.get("dummy", False):
o.num_objs -= 1 o.num_objs -= 1
o.obj_mapping[gid] = DummyObjectId.get(od.name).value
continue continue
# If not a dummy, increase sequential id for next object
# If this isn't a dummy object, increase object ID.
# ID's are sequential only for non-dummy objects.
obj_id += 1 obj_id += 1
# Get tile collision # Get tile collision
@ -78,9 +86,8 @@ def parse_tileset(firstgid: int, set_src: str) -> (ObjectMap, str):
# for p in points # for p in points
# ] # ]
# collision["points"] = points # collision["points"] = points
# Get other tile data
idx = i + firstgid o.obj_mapping[gid] = gid - firstgid
# Append TOML data # Append TOML data
if extra: if extra:
@ -103,7 +110,7 @@ def parse_tileset(firstgid: int, set_src: str) -> (ObjectMap, str):
# # TODO: append collision # # TODO: append collision
# pass # pass
o.object_types[idx] = od o.object_types[gid] = od
else: else:
o.num_objs -= 1 o.num_objs -= 1
@ -111,9 +118,61 @@ def parse_tileset(firstgid: int, set_src: str) -> (ObjectMap, str):
o.out = splitext(set_src)[0] + ".OTD" o.out = splitext(set_src)[0] + ".OTD"
o.is_level_specific = ts["name"] != "objects_common" o.is_level_specific = ts["name"] != "objects_common"
o.num_objs = len(o.object_types) o.num_objs = len(o.object_types)
o.name = ts["name"]
return (o, ts["name"]) return (o, ts["name"])
def parse_object_group(
tilesets: typing.Dict[str, ObjectMap], objgroup
) -> [ObjectPlacement]:
is_level_specific = False
current_ts = None
placements = []
objects = objgroup.find_all("object", [])
if not objects:
return []
# 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
is_level_specific = result
# If the tileset was not found... DON'T GO BEYOND THIS POINT!
assert current_ts is not None, "Object was not found in any tilesets!"
# Iterate over placements
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")))
p.x = int(float(obj.get("x")))
p.y = int(float(obj.get("y")))
p.flipx = False # TODO
p.flipy = False # TODO
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
pass
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], [ObjectPlacement]):
map = None map = None
with open(map_src) as f: with open(map_src) as f:
@ -132,5 +191,9 @@ def parse_map(map_src: str) -> (typing.Dict[str, ObjectMap], [ObjectPlacement]):
objmaps[ts_name] = loaded_set objmaps[ts_name] = loaded_set
# Retrieve objects placement # Retrieve objects placement
layergroup = map.find(name="group", attrs={"name": "OBJECTS"})
objgroups = layergroup.find_all("objectgroup")
for objgroup in objgroups:
placements += parse_object_group(objmaps, objgroup)
return (objmaps, placements) return (objmaps, placements)

View file

@ -1,6 +1,9 @@
// -*- mode: c; -*- // -*- mode: c; -*-
enum CommonId: u8 { enum CommonId: s8 {
RING_3V = -2,
RING_3H = -1,
RING = 0, RING = 0,
MONITOR = 1, MONITOR = 1,
SPIKES = 2, SPIKES = 2,

View file

@ -131,7 +131,7 @@ Frames are an array with the following arguments in order:
- [optional, default False] Flip_Y - [optional, default False] Flip_Y
#+begin_src toml :tangle ../assets/levels/COMMON/objects_common.toml #+begin_src toml
[ring] [ring]
[[ring.animations]] [[ring.animations]]
@ -421,7 +421,7 @@ dummy = true
** Parse test ** Parse test
#+begin_src python :results output #+begin_src python :results output :eval never
import toml import toml
from pprint import pp from pprint import pp