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

BMSCC Refactor #251

Merged
merged 16 commits into from
Dec 23, 2024
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
87 changes: 65 additions & 22 deletions src/mercury_engine_data_structures/formats/bmscc.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from __future__ import annotations

from typing import TYPE_CHECKING

import construct
from construct import (
from construct.core import (
Const,
Construct,
GreedyBytes,
IfThenElse,
Container,
Int8ul,
Int16ul,
Struct,
Expand All @@ -14,47 +15,89 @@

from mercury_engine_data_structures import game_check
from mercury_engine_data_structures.base_resource import BaseResource
from mercury_engine_data_structures.common_types import StrId, VersionAdapter, make_vector
from mercury_engine_data_structures.common_types import (
StrId,
VersionAdapter,
make_vector,
)
from mercury_engine_data_structures.construct_extensions.misc import ErrorWithMessage
from mercury_engine_data_structures.formats.collision import collision_formats
from mercury_engine_data_structures.game_check import Game

CollisionEntry = Struct(
name=StrId,
prop1=StrId,
prop2=StrId,
prop3=StrId,
flag=IfThenElse(
game_check.current_game_at_most(Game.SAMUS_RETURNS),

if TYPE_CHECKING:
from mercury_engine_data_structures.game_check import Game

Check warning on line 27 in src/mercury_engine_data_structures/formats/bmscc.py

View check run for this annotation

Codecov / codecov/patch

src/mercury_engine_data_structures/formats/bmscc.py#L27

Added line #L27 was not covered by tests

CollisionEntryData = Struct(
"name" / StrId,
"prop1" / StrId,
"prop2" / StrId,
"prop3" / StrId,
"flag"
/ game_check.is_sr_or_else(
Int8ul,
Int16ul,
),
type=StrId,
data=Switch(
"type" / StrId,
"data"
/ Switch(
construct.this.type,
collision_formats,
ErrorWithMessage(lambda ctx: f"Type {ctx.type} not known, valid types are {list(collision_formats.keys())}."),
),
)

CollisionLayer = Struct(
name=StrId,
entries=make_vector(CollisionEntry),
"name" / StrId,
"entries" / make_vector(CollisionEntryData),
)

PartsComponent = Struct(
"group" / StrId,
"components" / make_vector(Struct("name" / StrId)),
)

BMSCC = Struct(
_magic=Const(b"MSCD"),
_version=IfThenElse(
game_check.current_game_at_most(Game.SAMUS_RETURNS),
"_magic" / Const(b"MSCD"),
"_version"
/ game_check.is_sr_or_else(
VersionAdapter("1.13.0"),
VersionAdapter("1.16.0"),
),
layers=make_vector(CollisionLayer),
eof=GreedyBytes,
"layers" / make_vector(CollisionLayer),
"parts" / construct.Optional(make_vector(PartsComponent)),
construct.Terminated,
dyceron marked this conversation as resolved.
Show resolved Hide resolved
)


class CollisionEntry:
def __init__(self, raw: Container):
self._raw = raw

def get_data(self) -> Container:
"""Returns all data of collision/collision_camera"""
return self._raw.data

def get_poly(self, poly_idx: int):
"""Returns all data associated with a poly (points, boundings)"""
return self.get_data().polys[poly_idx]

def get_point(self, poly_idx: int, point_idx: int) -> Container:
"""Returns a specific point in a poly"""
return self.get_poly(poly_idx).points[point_idx]

def get_total_boundings(self) -> Container:
"""Returns the total boundary of collision/collision_camera"""
return self.get_data().total_boundings

def get_poly_boundings(self, poly_idx: int) -> Container:
"""Returns the boundary of a poly"""
return self.get_poly(poly_idx).boundings


class Bmscc(BaseResource):
@classmethod
def construct_class(cls, target_game: Game) -> Construct:
return BMSCC

# Bmscc has an entry per collision_camera, Bmscd has one entry per file
def get_entry(self, entry_idx: int = 0) -> CollisionEntry:
return CollisionEntry(self.raw.layers[0].entries[entry_idx])
29 changes: 29 additions & 0 deletions tests/formats/test_collision.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,32 @@ def test_compare_dread_210(dread_tree_210, file_path):
)
def test_compare_collision_msr(samus_returns_tree, file_path):
parse_build_compare_editor_parsed(Bmscc, samus_returns_tree, file_path)


@pytest.fixture()
def surface_bmscc(samus_returns_tree) -> Bmscc:
return samus_returns_tree.get_parsed_asset("maps/levels/c10_samus/s000_surface/s000_surface.bmscd", type_hint=Bmscc)


def test_get_data(surface_bmscc: Bmscc):
data = surface_bmscc.get_entry().get_data()
assert len(data) == 5


def test_modifying_collision(surface_bmscc: Bmscc):
point = surface_bmscc.get_entry().get_point(2, 9)
assert point["x"] == -800.0
assert point["y"] == -7000.0


def test_get_boundings(surface_bmscc: Bmscc):
total_boundings = surface_bmscc.get_entry().get_total_boundings()
polys = surface_bmscc.get_entry().get_data().polys
for i, poly in enumerate(polys):
poly_boundings = surface_bmscc.get_entry().get_poly_boundings(i)
# Boundings for polygons are in the order: x1, y1, x2, y2
# Assert that the boundings are confined within the total bounds of the collision_camera
assert poly_boundings[0] >= total_boundings[0]
assert poly_boundings[1] >= total_boundings[1]
assert poly_boundings[2] <= total_boundings[2]
assert poly_boundings[3] <= total_boundings[3]
Loading