Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add classes to BMSMSD #265

Merged
merged 7 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

Check warning on line 112 in src/mercury_engine_data_structures/formats/bmsmsd.py

View check run for this annotation

Codecov / codecov/patch

src/mercury_engine_data_structures/formats/bmsmsd.py#L112

Added line #L112 was not covered by tests

@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

Check warning on line 120 in src/mercury_engine_data_structures/formats/bmsmsd.py

View check run for this annotation

Codecov / codecov/patch

src/mercury_engine_data_structures/formats/bmsmsd.py#L120

Added line #L120 was not covered by tests

@property
def icon(self) -> str:
return self._raw.icon

@icon.setter
def icon(self, value: str) -> None:
self._raw.icon = value

Check warning on line 128 in src/mercury_engine_data_structures/formats/bmsmsd.py

View check run for this annotation

Codecov / codecov/patch

src/mercury_engine_data_structures/formats/bmsmsd.py#L128

Added line #L128 was not covered by tests

@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

Check warning on line 136 in src/mercury_engine_data_structures/formats/bmsmsd.py

View check run for this annotation

Codecov / codecov/patch

src/mercury_engine_data_structures/formats/bmsmsd.py#L136

Added line #L136 was not covered by tests

@property
def coordinates(self) -> Vec3:
return self._raw.coordinates

@coordinates.setter
def coordinates(self, value: Vec3) -> None:
self._raw.coordinates = value

Check warning on line 144 in src/mercury_engine_data_structures/formats/bmsmsd.py

View check run for this annotation

Codecov / codecov/patch

src/mercury_engine_data_structures/formats/bmsmsd.py#L144

Added line #L144 was not covered by tests


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

Check warning on line 173 in src/mercury_engine_data_structures/formats/bmsmsd.py

View check run for this annotation

Codecov / codecov/patch

src/mercury_engine_data_structures/formats/bmsmsd.py#L173

Added line #L173 was not covered by tests

@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)
Loading