diff --git a/src/mercury_engine_data_structures/adapters/flagsenum_adapter.py b/src/mercury_engine_data_structures/adapters/flagsenum_adapter.py new file mode 100644 index 00000000..b14d37fc --- /dev/null +++ b/src/mercury_engine_data_structures/adapters/flagsenum_adapter.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from construct import Adapter, FlagsEnum, Int32ub + + +class FlagsEnumAdapter(Adapter): + def __init__(self, enum_class, subcon=Int32ub): + super().__init__(FlagsEnum(subcon, enum_class)) + self._enum_class = enum_class + + def _decode(self, obj, context, path): + return {self._enum_class[k]: v for k, v in obj.items() if k != "_flagsenum" and v is True} + + def _encode(self, obj, context, path): + return {k.name: v for k, v in obj.items()} diff --git a/src/mercury_engine_data_structures/common_types.py b/src/mercury_engine_data_structures/common_types.py index 86e66f46..a70125ea 100644 --- a/src/mercury_engine_data_structures/common_types.py +++ b/src/mercury_engine_data_structures/common_types.py @@ -159,6 +159,7 @@ def _emitbuild(self, code: construct.CodeGen) -> str: CVector3D = CVectorConstruct(3) CVector4D = CVectorConstruct(4) Transform3D = construct.Struct("position" / CVector3D, "rotation" / CVector3D, "scale" / CVector3D) +BoundingBox2D = construct.Struct("min" / CVector2D, "max" / CVector2D) class VersionAdapter(Adapter): diff --git a/src/mercury_engine_data_structures/formats/bmsmsd.py b/src/mercury_engine_data_structures/formats/bmsmsd.py index f3b9da4c..c4687d4b 100644 --- a/src/mercury_engine_data_structures/formats/bmsmsd.py +++ b/src/mercury_engine_data_structures/formats/bmsmsd.py @@ -1,6 +1,7 @@ from __future__ import annotations import functools +from enum import Enum from typing import TYPE_CHECKING import construct @@ -8,19 +9,21 @@ Const, Construct, Container, - Enum, - FlagsEnum, - Float32l, Int32sl, Int32ul, Struct, ) +from mercury_engine_data_structures.adapters.enum_adapter import EnumAdapter +from mercury_engine_data_structures.adapters.flagsenum_adapter import FlagsEnumAdapter from mercury_engine_data_structures.base_resource import BaseResource from mercury_engine_data_structures.common_types import ( + BoundingBox2D, CVector2D, CVector3D, StrId, + Vec2, + Vec3, VersionAdapter, make_vector, ) @@ -28,65 +31,65 @@ if TYPE_CHECKING: from mercury_engine_data_structures.game_check import Game -TileBorders = FlagsEnum( - Int32sl, - TOP=1, - BOTTOM=2, - LEFT=4, - RIGHT=8, - OPEN_TOP=16, - OPEN_BOTTOM=32, - OPEN_LEFT=64, - OPEN_RIGHT=128, -) -TileType = Enum( - Int32ul, - NORMAL=1, - HEAT=2, - ACID=4, - ACID_RISE=8, - ACID_FALL=12, -) +class TileBorder(int, Enum): + TOP = 1 + BOTTOM = 2 + LEFT = 4 + RIGHT = 8 + OPEN_TOP = 16 + OPEN_BOTTOM = 32 + OPEN_LEFT = 64 + OPEN_RIGHT = 128 -IconPriority = Enum( - Int32sl, - METROID=-1, - ACTOR=0, - SHIP=1, - ENERGY_CLOUD=2, - DOOR=3, - CHOZO_SEAL=4, - HIDDEN_ITEM=5, -) + +TileBorderConstruct = FlagsEnumAdapter(TileBorder, Int32sl) + + +class TileType(int, Enum): + NORMAL = 1 + HEAT = 2 + ACID = 4 + ACID_RISE = 8 + ACID_FALL = 12 + + +TileTypeConstruct = EnumAdapter(TileType, Int32ul) + + +class IconPriority(int, Enum): + METROID = -1 + ACTOR = 0 + SHIP = 1 + ENERGY_CLOUD = 2 + DOOR = 3 + CHOZO_SEAL = 4 + HIDDEN_ITEM = 5 + + +IconPriorityConstruct = EnumAdapter(IconPriority, Int32sl) # BMSMSD BMSMSD = Struct( "_magic" / Const(b"MMSD"), "version" / VersionAdapter("1.7.0"), "scenario" / StrId, - "tile_size" / construct.Array(2, Float32l), + "tile_size" / CVector2D, "x_tiles" / Int32sl, "y_tiles" / Int32sl, - "map_dimensions" / Struct( - "bottom_left" / CVector2D, - "top_right" / CVector2D, - ), + "map_dimensions" / BoundingBox2D, "tiles" / make_vector( Struct( "tile_coordinates" / construct.Array(2, Int32sl), - "tile_dimension" / Struct( - "bottom_left" / CVector2D, - "top_right" / CVector2D, - ), - "tile_borders" / TileBorders, - "tile_type" / TileType, + "tile_dimensions" / BoundingBox2D, + "tile_borders" / TileBorderConstruct, + "tile_type" / TileTypeConstruct, "icons" / make_vector( Struct( "actor_name" / StrId, "clear_condition" / StrId, "icon" / StrId, - "icon_priority" / IconPriority, + "icon_priority" / IconPriorityConstruct, "coordinates" / CVector3D, ) ), @@ -96,11 +99,119 @@ ) # fmt: skip +class IconProperties: + def __init__(self, raw: Container) -> None: + self._raw = raw + + @property + def actor_name(self) -> str: + return self._raw.actor_name + + @actor_name.setter + def actor_name(self, value: str) -> None: + self._raw.actor_name = value + + @property + def clear_condition(self) -> str: + return self._raw.clear_condition + + @clear_condition.setter + def clear_condition(self, value: str) -> None: + self._raw.clear_condition = value + + @property + def icon(self) -> str: + return self._raw.icon + + @icon.setter + def icon(self, value: str) -> None: + self._raw.icon = value + + @property + def icon_priority(self) -> IconPriority: + return self._raw.icon_priority + + @icon_priority.setter + def icon_priority(self, value: IconPriority) -> None: + self._raw.icon_priority = value + + @property + def coordinates(self) -> Vec3: + return self._raw.coordinates + + @coordinates.setter + def coordinates(self, value: Vec3) -> None: + self._raw.coordinates = value + + +class TileProperties: + def __init__(self, raw: Container) -> None: + self._raw = raw + + @property + def tile_coordinates(self) -> list[int]: + return self._raw.tile_coordinates + + @tile_coordinates.setter + def tile_coordinates(self, value: list[int]) -> None: + self._raw.tile_coordinates = value + + @property + def tile_dimensions(self) -> dict[Vec2, Vec2]: + return self._raw.tile_dimensions + + @tile_dimensions.setter + def tile_dimensions(self, value: dict[Vec2, Vec2]) -> None: + self._raw.tile_dimensions = value + + @property + def tile_borders(self) -> dict[TileBorder, bool]: + return self._raw.tile_borders + + @tile_borders.setter + def tile_borders(self, border_type: dict[TileBorder, bool], value: bool) -> None: + self._raw.tile_borders[border_type] = value + + @property + def tile_type(self) -> TileType: + return self._raw.tile_type + + @tile_type.setter + def tile_type(self, value: TileType): + self._raw.tile_type = value + + def get_icon(self, icon_idx: int = 0) -> IconProperties: + return IconProperties(self._raw.icons[icon_idx]) + + def add_icon( + self, + actor_name: str, + clear_condition: str, + icon: str, + icon_priority: str, + coordinates: Vec3, + ) -> Container: + new_icon = Container( + { + "actor_name": actor_name, + "clear_condition": clear_condition, + "icon": icon, + "icon_priority": icon_priority, + "coordinates": coordinates, + } + ) + + self._raw.icons.append(new_icon) + + def remove_icon(self, icon_idx: int = 0) -> None: + self._raw.icons.pop(icon_idx) + + class Bmsmsd(BaseResource): @classmethod @functools.lru_cache def construct_class(cls, target_game: Game) -> Construct: return BMSMSD - def get_tile(self, tile_idx: int) -> Container: - return self.raw.tiles[tile_idx] + def get_tile(self, tile_idx: int) -> TileProperties: + return TileProperties(self.raw.tiles[tile_idx]) diff --git a/tests/formats/test_bmsmsd.py b/tests/formats/test_bmsmsd.py index e6257c3e..c96f0ff3 100644 --- a/tests/formats/test_bmsmsd.py +++ b/tests/formats/test_bmsmsd.py @@ -4,7 +4,8 @@ from tests.test_lib import parse_build_compare_editor from mercury_engine_data_structures import samus_returns_data -from mercury_engine_data_structures.formats.bmsmsd import Bmsmsd, TileType +from mercury_engine_data_structures.common_types import Vec2, Vec3 +from mercury_engine_data_structures.formats.bmsmsd import Bmsmsd, IconPriority, TileBorder, TileType @pytest.mark.parametrize("bmsmsd_path", samus_returns_data.all_files_ending_with(".bmsmsd")) @@ -21,8 +22,63 @@ def test_get_tile(surface_bmsmsd: Bmsmsd): tile = surface_bmsmsd.get_tile(4) assert tile.tile_coordinates == [48, 5] - tile = surface_bmsmsd.get_tile(12) - assert len(tile.icons) == 2 + tile = surface_bmsmsd.get_tile(0) + assert tile.tile_dimensions == {"min": Vec2(6400.0, -10600.0), "max": Vec2(7000.0, -9800.0)} + + tile = surface_bmsmsd.get_tile(8) + assert tile.tile_borders == { + TileBorder.TOP: True, + TileBorder.BOTTOM: True, + TileBorder.RIGHT: True, + } tile = surface_bmsmsd.get_tile(25) assert tile.tile_type == TileType.NORMAL + + tile = surface_bmsmsd.get_tile(12) + assert tile.get_icon(0) is not None + assert tile.get_icon(1) is not None + + +def test_set_tile_properties(surface_bmsmsd: Bmsmsd): + tile = surface_bmsmsd.get_tile(0) + + tile.tile_coordinates = [30, 20] + assert tile.tile_coordinates == [30, 20] + + tile.tile_dimensions = {"min": Vec2(10000.0, -1000.0), "max": Vec2(50000.0, -29000.0)} + assert tile.tile_dimensions == {"min": Vec2(10000.0, -1000.0), "max": Vec2(50000.0, -29000.0)} + + tile.tile_borders[TileBorder.OPEN_TOP] = True + assert tile.tile_borders[TileBorder.OPEN_TOP] is True + + tile.tile_type = TileType.ACID_FALL + assert tile.tile_type is TileType.ACID_FALL + + +def test_get_icon(surface_bmsmsd: Bmsmsd): + icon = surface_bmsmsd.get_tile(4).get_icon() + assert icon.actor_name == "LE_Item_001" + assert icon.clear_condition == "" + assert icon.icon == "item_missiletank" + assert icon.icon_priority is IconPriority.ACTOR + assert icon.coordinates == Vec3(-5500.0, -9700.0, 0.0) + + +def test_add_icon(surface_bmsmsd: Bmsmsd): + tile = surface_bmsmsd.get_tile(10) + assert tile is not None + + tile.add_icon("LE_Test_Icon", "CollectItem", "itemsphere", IconPriority.ACTOR, Vec3(100.0, 100.0, 0.0)) + + icon = tile.get_icon(1) + assert icon.actor_name == "LE_Test_Icon" + assert icon.clear_condition == "CollectItem" + assert icon.icon == "itemsphere" + assert icon.icon_priority is IconPriority.ACTOR + assert icon.coordinates == Vec3(100.0, 100.0, 0.0) + + +def test_remove_icon(surface_bmsmsd: Bmsmsd): + tile = surface_bmsmsd.get_tile(1) + tile.remove_icon(0)