Skip to content

Commit

Permalink
Merge pull request #50 from randovania/feature/bmsad-sr
Browse files Browse the repository at this point in the history
Progress in BMSAD for SR
  • Loading branch information
duncathan authored Aug 4, 2023
2 parents 10435d3 + ac5d129 commit bba6e58
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 32 deletions.
8 changes: 8 additions & 0 deletions src/mercury_engine_data_structures/common_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,24 @@ def items(self):


class DictAdapter(Adapter):
def __init__(self, subcon, *, allow_duplicates: bool = False):
super().__init__(subcon)
self.allow_duplicates = allow_duplicates

def _decode(self, obj: construct.ListContainer, context, path):
result = construct.Container()
for item in obj:
key = item.key
if key in result:
if self.allow_duplicates:
return obj
raise construct.ConstructError(f"Key {key} found twice in object", path)
result[key] = item.value
return result

def _encode(self, obj: construct.Container, context, path):
if self.allow_duplicates and isinstance(obj, list):
return obj
return construct.ListContainer(
construct.Container(key=type_, value=item)
for type_, item in obj.items()
Expand Down
175 changes: 147 additions & 28 deletions src/mercury_engine_data_structures/formats/bmsad.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
Float32l,
Hex,
IfThenElse,
Int8ul,
Int16ul,
Int32ul,
PrefixedArray,
Expand All @@ -23,6 +24,15 @@
from mercury_engine_data_structures.formats.property_enum import PropertyEnum
from mercury_engine_data_structures.game_check import Game


def SR_or_Dread(sr, dread):
return IfThenElse(
game_check.current_game_at_most(Game.SAMUS_RETURNS),
sr,
dread,
)


FunctionArgument = Struct(
type=Char,
value=Switch(
Expand All @@ -40,7 +50,11 @@
name=StrId,
unk=Int16ul,
params=common_types.DictAdapter(common_types.make_vector(
common_types.DictElement(FunctionArgument, key=PropertyEnum)
common_types.DictElement(
FunctionArgument,
# FIXME: PropertyEnum should properly support SR
key=SR_or_Dread(Hex(Int32ul), PropertyEnum),
)
)),
))

Expand All @@ -60,7 +74,7 @@ def find_charclass_for_type(type_name: str):
)


def Dependencies():
def DreadDependencies():
component_dependencies = {
"CFXComponent": make_vector(Struct(
"file" / StrId,
Expand Down Expand Up @@ -118,7 +132,26 @@ def component_type(this):
return Switch(component_type, component_dependencies)


Component = Struct(
ExtraFields = common_types.DictAdapter(common_types.make_vector(
common_types.DictElement(Struct(
"type" / StrId,
"value" / Switch(
construct.this.type,
{
"bool": Flag,
"string": StrId,
"float": Float,

"int": construct.Int32sl,
"vec3": Float[3],
},
ErrorWithMessage(lambda ctx: f"Unknown argument type: {ctx.type}", construct.SwitchError)
)
))
), allow_duplicates=True)


DreadComponent = Struct(
type=StrId,
unk_1=Array(2, Hex(Int32ul)),
fields=PrefixedAllowZeroLen(
Expand All @@ -135,22 +168,10 @@ def component_type(this):
),
extra_fields=construct.If(
lambda this: type_lib.is_child_of(this.type, "CComponent"),
common_types.DictAdapter(common_types.make_vector(
common_types.DictElement(Struct(
"type" / StrId,
"value" / Switch(
construct.this.type,
{
"bool": Flag,
"string": StrId
},
ErrorWithMessage(lambda ctx: f"Unknown argument type: {ctx.type}", construct.SwitchError)
)
))
))
ExtraFields,
),
functions=Functions,
dependencies=Dependencies()
dependencies=DreadDependencies(),
)

CCharClass = Struct(
Expand All @@ -165,7 +186,7 @@ def component_type(this):
unk_6=StrId,
unk_7=Byte,

components=make_dict(Component),
components=make_dict(DreadComponent),

binaries=make_vector(StrId),
sources=make_vector(StrId >> Byte),
Expand All @@ -178,7 +199,7 @@ def component_type(this):
sub_actors=PrefixedArray(Int32ul, StrId),
unk_4=StrId,

components=make_dict(Component),
components=make_dict(DreadComponent),

binaries=make_vector(StrId),
sources=make_vector(StrId >> Byte),
Expand All @@ -189,13 +210,113 @@ def component_type(this):
"CActorDef": CActorDef
}
#
BMSAD = Struct(


def SRDependencies():
component_dependencies = {
"CAnimationComponent": make_vector(StrId),
"CFXComponent": make_vector(Struct(
"file" / StrId,
"unk1" / Int32ul,
"unk2" / Int32ul,
)),
"CCollisionComponent": Struct(
"file" / StrId,
"unk" / Int16ul
),
"CGrabComponent": make_vector(Struct(
a=StrId,
b=StrId,
c=StrId,
d=Hex(Int32ul)[2],
e=Float[8],
f=Hex(Int32ul)[9],
)),
"CGlowflyAnimationComponent": make_vector(StrId),
"CSceneModelAnimationComponent": make_dict(make_dict(StrId)),

"CBillboardComponent": Struct(
"id1" / StrId,
"unk1" / make_dict(Struct(
"unk1" / Int32ul[3],
"unk2" / Byte,
"unk3" / Int32ul[2],
"unk4" / Float32l
)),
"id2" / StrId,
"unk3" / make_vector(Struct(
"id" / StrId,
"unk1" / Byte,
"unk2" / make_vector(Struct(
"a" / Float,
"b" / Float,
"c" / Float,
)),
)),
),

"CSwarmControllerComponent": Struct(
"unk1" / make_vector(StrId),
"unk2" / make_vector(StrId),
"unk3" / make_vector(StrId)
),
}
for dep in [
"CTsumuriAcidDroolCollision",
"CBillboardCollisionComponent",
"CQueenPlasmaArcCollision",
]:
component_dependencies[dep] = component_dependencies["CCollisionComponent"]

for dep in [
"CFlockingSwarmControllerComponent",
"CBeeSwarmControllerComponent",
]:
component_dependencies[dep] = component_dependencies["CSwarmControllerComponent"]

return Switch(construct.this.type, component_dependencies)


SR_Component = Struct(
type=StrId,
unk_1=Hex(Int32ul)[2],
functions=Functions,

extra_fields=ExtraFields,
dependencies=SRDependencies(),
)

BMSAD_SR = Struct(
_magic=Const(b"MSAD"),
version=IfThenElse(
game_check.current_game_at_most(Game.SAMUS_RETURNS),
Const(0x002C0001, Hex(Int32ul)),
Const(0x0200000F, Hex(Int32ul)),
),
version=Const(0x002C0001, Hex(Int32ul)),

name=StrId,
model_name=StrId,

unk_1=Int8ul,
unk_2=Float[9],
unk_3=Int8ul,
unk_4=Int32ul,
other_magic=Const(0xFFFFFFFF, Hex(Int32ul)),
unk_5=Int16ul,
unk_6=StrId[2],
unk_7=Int8ul,
sub_actors=make_vector(StrId),
unk_8=Int32ul,

# count=Int32ul,
# component=(StrId >> SR_Component)[4],
components=make_dict(SR_Component),

binaries=make_vector(StrId),

rest=construct.GreedyBytes,
_end=construct.Terminated,
)

BMSAD_Dread = Struct(
_magic=Const(b"MSAD"),
version=Const(0x0200000F, Hex(Int32ul)),

# # gameeditor::CGameModelRoot
# root_type=construct.Const('Root', PropertyEnum),
Expand All @@ -209,11 +330,9 @@ def component_type(this):
property_types,
ErrorWithMessage(lambda ctx: f"Unknown property type: {ctx.type}"),
),
# rest=Peek(construct.GreedyBytes),

# z=Probe(),
_end=construct.Terminated,
)
BMSAD = SR_or_Dread(BMSAD_SR, BMSAD_Dread)


# BMSAD = game_model_root.create('CActorDef', 0x02000031)
Expand Down
7 changes: 6 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def get_env_or_skip(env_name):
return os.environ[env_name]


@pytest.fixture()
@pytest.fixture(scope="session")
def samus_returns_path():
return Path(get_env_or_skip("SAMUS_RETURNS_PATH"))

Expand All @@ -28,6 +28,11 @@ def dread_path():
return Path(get_env_or_skip("DREAD_PATH"))


@pytest.fixture(scope="session")
def samus_returns_tree(samus_returns_path):
return FileTreeEditor(samus_returns_path, Game.SAMUS_RETURNS)


@pytest.fixture(scope="session")
def dread_file_tree(dread_path):
return FileTreeEditor(dread_path, Game.DREAD)
Expand Down
29 changes: 26 additions & 3 deletions tests/formats/test_bmsad.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,27 @@
import pytest
from tests.test_lib import parse_build_compare_editor

from mercury_engine_data_structures import dread_data
from mercury_engine_data_structures import dread_data, samus_returns_data
from mercury_engine_data_structures.formats.bmsad import Bmsad

all_sr_bmsad = [name for name in samus_returns_data.all_name_to_asset_id().keys()
if name.endswith(".bmsad")]

all_dread_bmsad = [name for name in dread_data.all_name_to_asset_id().keys()
if name.endswith(".bmsad")]

expected_failures = {
expected_dread_failures = {
"actors/props/pf_mushr_fr/charclasses/pf_mushr_fr.bmsad",
}
expected_sr_failures = {
"actors/items/adn/charclasses/adn.bmsad",
"actors/props/ridleystorm/charclasses/ridleystorm.bmsad",
}


@pytest.mark.parametrize("bmsad_path", all_dread_bmsad)
def test_compare_dread_all(dread_file_tree, bmsad_path):
if bmsad_path in expected_failures:
if bmsad_path in expected_dread_failures:
expectation = pytest.raises(construct.ConstructError)
else:
expectation = contextlib.nullcontext()
Expand All @@ -26,3 +33,19 @@ def test_compare_dread_all(dread_file_tree, bmsad_path):
parse_build_compare_editor(
Bmsad, dread_file_tree, bmsad_path
)


@pytest.mark.parametrize("bmsad_path", all_sr_bmsad)
def test_compare_sr_all(samus_returns_tree, bmsad_path):
if not samus_returns_tree.does_asset_exists(bmsad_path):
return pytest.skip()

if bmsad_path in expected_sr_failures:
expectation = pytest.raises(construct.core.ConstError)
else:
expectation = contextlib.nullcontext()

with expectation:
parse_build_compare_editor(
Bmsad, samus_returns_tree, bmsad_path
)

0 comments on commit bba6e58

Please sign in to comment.