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 and functions to BMDEFS #266

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
179 changes: 162 additions & 17 deletions src/mercury_engine_data_structures/formats/bmdefs.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
from __future__ import annotations

import construct
from construct import (
from construct.core import (
Const,
Construct,
Container,
Flag,
Float32l,
Int32ul,
Struct,
)

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_dict, make_vector
from mercury_engine_data_structures.formats import standard_format
from mercury_engine_data_structures.game_check import Game

Expand All @@ -38,10 +39,7 @@
"unk4" / Int32ul,
"unk_bool" / Flag,
"environment_sfx_volume" / Float32l,
"inner_states" / make_vector(Struct(
"type" / StrId,
"unk1" / Float32l,
))
"inner_states" / make_dict(Float32l)
),
'DEATH': Struct(
"unk1" / Int32ul,
Expand All @@ -55,10 +53,7 @@
"unk4" / Int32ul,
"unk_bool" / Flag,
"environment_sfx_volume" / Float32l,
"inner_states" / make_vector(Struct(
"type" / StrId,
"unk1" / Float32l,
))
"inner_states" / make_dict(Float32l)
),
},
)
Expand All @@ -68,10 +63,11 @@
) # fmt: skip

BMDEFS = Struct(
_magic=Const(b"MDEF"),
version=VersionAdapter("1.5.0"),
unk1=Int32ul,
sounds=make_vector(
"_magic" / Const(b"MDEF"),
"version" / VersionAdapter("1.5.0"),
"number_of_sounds" / Int32ul,
"sounds"
/ make_vector(
Struct(
"sound_name" / StrId,
"unk1" / Int32ul,
Expand All @@ -87,16 +83,165 @@
"environment_sfx_volume" / Float32l,
)
), # fmt: skip
unk2=Int32ul,
enemies_list=make_vector(EnemyStruct),
rest=construct.GreedyBytes,
"number_of_enemy_groups" / Int32ul,
"enemies_list" / make_vector(EnemyStruct),
construct.Terminated,
)


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

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

@state_type.setter
def state_type(self, value: str) -> None:
self._raw.type = value
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could make the type an Enum instead of an arbitrary string, right? Because only DEATH or COMBAT should be accepted values.


def get_sound_properties(self) -> Sounds:
return Sounds(self._raw.properties)


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

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

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

def get_state(self, state_idx: int) -> EnemyStates:
return EnemyStates(self._raw.states[state_idx])


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

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

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

def get_layer(self, layer_idx: int) -> EnemyLayers:
return EnemyLayers(self._raw.layers[layer_idx])


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

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

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

def get_area(self, area_idx: int) -> Areas:
return Areas(self._raw.areas[area_idx])


class Sounds:
def __init__(self, raw: Container) -> None:
self._raw = raw

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

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

@property
def priority(self) -> int:
return self._raw.priority

Check warning on line 170 in src/mercury_engine_data_structures/formats/bmdefs.py

View check run for this annotation

Codecov / codecov/patch

src/mercury_engine_data_structures/formats/bmdefs.py#L170

Added line #L170 was not covered by tests

@priority.setter
def priority(self, value: str) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value: int

self._raw.priority = value

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

Check warning on line 178 in src/mercury_engine_data_structures/formats/bmdefs.py

View check run for this annotation

Codecov / codecov/patch

src/mercury_engine_data_structures/formats/bmdefs.py#L178

Added line #L178 was not covered by tests

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

@property
def fade_in(self) -> float:
return self._raw.fade_in

Check warning on line 186 in src/mercury_engine_data_structures/formats/bmdefs.py

View check run for this annotation

Codecov / codecov/patch

src/mercury_engine_data_structures/formats/bmdefs.py#L186

Added line #L186 was not covered by tests

@fade_in.setter
def fade_in(self, value: float) -> None:
self._raw.fade_in = value

@property
def fade_out(self) -> float:
return self._raw.fade_out

@fade_out.setter
def fade_out(self, value: float) -> None:
self._raw.fade_out = value

@property
def volume(self) -> float:
return self._raw.volume

@volume.setter
def volume(self, value: float) -> None:
self._raw.volume = value

@property
def environment_sfx_volume(self) -> float:
return self._raw.environment_sfx_volume

Check warning on line 210 in src/mercury_engine_data_structures/formats/bmdefs.py

View check run for this annotation

Codecov / codecov/patch

src/mercury_engine_data_structures/formats/bmdefs.py#L210

Added line #L210 was not covered by tests

@environment_sfx_volume.setter
def environment_sfx_volume(self, value: float) -> None:
self._raw.environment_sfx_volume = value

# inner_states and start_delay are only used for enemies
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then make a seperate child class (something something named like EnemySounds) inheriting from Sounds which has these extra methods.

@property
def start_delay(self) -> float:
return self._raw.start_delay

@start_delay.setter
def start_delay(self, value: float) -> None:
self._raw.start_delay = value

@property
def inner_states(self) -> dict[str, float]:
return self._raw.inner_states

@inner_states.setter
def inner_states(self, value: dict[str, float]) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The str key in dict[str, float] should be restricted to valid values (RELAX and DEATH only?). We can use an enum here, do we? (I'm tired and my brain starts to mix programming languages. Using something like dict[InnerStatEnum, float] is doable but I don't know if it gets encoded properly without additional things)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would just need to change the make_dict(Float32l) to make_dict(Float32l, key_type=EnumAdapter(InnerStateEnum, StrId)) to decode/encode the way you'd like

for name, value in value.items():
self._raw.inner_states[name] = value


class Bmdefs(BaseResource):
@classmethod
def construct_class(cls, target_game: Game) -> Construct:
if target_game == Game.SAMUS_RETURNS:
return BMDEFS
else:
return standard_format.game_model("sound::CMusicManager", "4.0.2")

def get_sound(self, sound_idx: int) -> Sounds:
return Sounds(self.raw.sounds[sound_idx])

def get_enemy(self, enemy_idx: int) -> EnemiesList:
return EnemiesList(self.raw.enemies_list[enemy_idx])
77 changes: 77 additions & 0 deletions tests/formats/test_bmdefs.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from __future__ import annotations

import copy

import pytest
from tests.test_lib import parse_build_compare_editor

from mercury_engine_data_structures.formats.bmdefs import Bmdefs
Expand All @@ -11,3 +14,77 @@ def test_bmdefs_dread(dread_tree_100):

def test_bmdefs_sr(samus_returns_tree):
parse_build_compare_editor(Bmdefs, samus_returns_tree, "system/snd/scenariomusicdefs.bmdefs")


@pytest.fixture()
def bmdefs(samus_returns_tree) -> Bmdefs:
return samus_returns_tree.get_parsed_asset("system/snd/scenariomusicdefs.bmdefs", type_hint=Bmdefs)


def test_get_sound(bmdefs: Bmdefs):
sound = bmdefs.get_sound(0)
assert sound.sound_name == "matad_jintojo_32728k"
assert sound.volume == 1.0


def test_set_sound_properties(bmdefs: Bmdefs):
sound = bmdefs.get_sound(3)
assert sound is not None

original_properties = copy.deepcopy(sound)

sound.sound_name = "m_new_sound"
sound.priority = 1
sound.file_path = "a/real/file/path.wav"
sound.fade_in = 0.1
sound.fade_out = 4.0
sound.volume = 0.5
sound.environment_sfx_volume = 0.79

assert sound != original_properties


def test_get_enemy(bmdefs: Bmdefs):
enemy = bmdefs.get_enemy(0)
assert enemy.enemy_name == "alphanewborn"

area = enemy.get_area(0)
assert area.area_name == "s000_surface"

layer = area.get_layer(0)
assert layer.layer_name == "default"

state = layer.get_state(0)
assert state.state_type == "COMBAT"

sound_properties = state.get_sound_properties()
assert sound_properties.start_delay == 0.0
assert sound_properties.inner_states == {"RELAX": 3.0, "DEATH": 5.0}


def test_set_enemy_properties(bmdefs: Bmdefs):
enemy = bmdefs.get_enemy(9)
assert enemy is not None

enemy.enemy_name = "kraid"
assert enemy.enemy_name != "queen"

area = enemy.get_area(0)
area.area_name = "s050_area5"
assert area != "s100_area10"

layer = area.get_layer(0)
layer.layer_name = "not_default"
assert layer.layer_name != "default"

state = layer.get_state(1)
state.state_type = "COMBAT"
assert state.state_type != "DEATH"

sound_properties = state.get_sound_properties()
sound_properties.fade_out = 10.0
assert sound_properties.fade_out != 3.0
sound_properties.start_delay = 0.4
assert sound_properties.start_delay != 2.0
sound_properties.inner_states = {"RELAX": 1.0, "DEATH": 45.0}
assert sound_properties.inner_states != {}
Loading