Skip to content

Commit

Permalink
Merge pull request #265 from randovania/bmsmsd
Browse files Browse the repository at this point in the history
Add classes to BMSMSD
  • Loading branch information
dyceron authored Jan 17, 2025
2 parents 516335d + 612ef65 commit 0627881
Show file tree
Hide file tree
Showing 4 changed files with 232 additions and 49 deletions.
15 changes: 15 additions & 0 deletions src/mercury_engine_data_structures/adapters/flagsenum_adapter.py
Original file line number Diff line number Diff line change
@@ -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()}
1 change: 1 addition & 0 deletions src/mercury_engine_data_structures/common_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
203 changes: 157 additions & 46 deletions src/mercury_engine_data_structures/formats/bmsmsd.py
Original file line number Diff line number Diff line change
@@ -1,92 +1,95 @@
from __future__ import annotations

import functools
from enum import Enum
from typing import TYPE_CHECKING

import construct
from construct.core import (
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,
)

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,
)
),
Expand All @@ -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])
62 changes: 59 additions & 3 deletions tests/formats/test_bmsmsd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand All @@ -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)

0 comments on commit 0627881

Please sign in to comment.