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

Refactor BRFLD #217

Merged
merged 34 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
976c4c1
Refactor BRFLD
MayberryZoom Aug 31, 2024
ab062d4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 31, 2024
cfef4c3
Rename layer to actor_layer and use enum for it
MayberryZoom Oct 7, 2024
a84a10f
Merge branch 'main' into brfld-layers
MayberryZoom Oct 7, 2024
2c49930
Get actor group by actor layer in add/remove actor from group functions
MayberryZoom Oct 11, 2024
64d002c
Use strings in ActorLayer enum
MayberryZoom Oct 11, 2024
96da27b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 11, 2024
c37ecb2
Add method to add actor to actor groups in other actor layers
MayberryZoom Oct 12, 2024
0ac279c
Add test for adding actor to actor groups
MayberryZoom Oct 12, 2024
428922d
Remove add_actor_to_entity_groups method
MayberryZoom Oct 12, 2024
beeee14
Rename actor_layer_name to actor_layer
MayberryZoom Oct 12, 2024
f340a7d
Get actor groups with correct method in add_actor_to_actor_groups
MayberryZoom Oct 12, 2024
a427581
Missing argument when getting actor group in is_actor_in_group
MayberryZoom Oct 12, 2024
b7fb6e4
Use enum value to access actor layers
MayberryZoom Oct 12, 2024
8954d9b
Fix collision camera names in tests
MayberryZoom Oct 12, 2024
aec9a37
Add missing type hints
MayberryZoom Oct 12, 2024
eb364ce
Add and update docstrings
MayberryZoom Oct 12, 2024
8bbc2eb
Fix ruff errors
MayberryZoom Oct 12, 2024
4e063d1
Add additional tests
MayberryZoom Oct 12, 2024
b7d9c1d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 12, 2024
c51f244
Merge branch 'main' into brfld-layers
MayberryZoom Oct 14, 2024
1ef9c69
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 14, 2024
1bced76
Re-add missing import
MayberryZoom Oct 14, 2024
9bc3a5d
Make ActorLayer enum extend str
MayberryZoom Oct 16, 2024
5fe16d4
Parametrize add/remove actor from group tests
MayberryZoom Oct 16, 2024
d3907cc
Raise and test exception when adding/removing actor from group
MayberryZoom Oct 16, 2024
8b20e2f
Use ActorLink alias when a method returns an actor link str
MayberryZoom Oct 16, 2024
ffef029
Fix ruff errors
MayberryZoom Oct 16, 2024
1272f96
Update all_actors_in_actor_layer returns docstring
MayberryZoom Oct 16, 2024
823a9cc
Merge branch 'main' into brfld-layers
MayberryZoom Oct 16, 2024
717c24b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 16, 2024
6ee9314
Add name and level properties to BRFLD
MayberryZoom Oct 16, 2024
4e90100
Use optional regex for exceptions in BRFLD test
MayberryZoom Oct 17, 2024
7f3b6f7
Rename ActorLink to BrfldLink and hint it in follow_link argument
MayberryZoom Oct 17, 2024
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
181 changes: 148 additions & 33 deletions src/mercury_engine_data_structures/formats/brfld.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

import functools
import logging
from typing import TYPE_CHECKING
from collections.abc import Iterator
from enum import Enum
from typing import TYPE_CHECKING, Any

import construct

from mercury_engine_data_structures.base_resource import BaseResource
from mercury_engine_data_structures.formats import standard_format
Expand All @@ -16,67 +20,178 @@

logger = logging.getLogger(__name__)

BrfldLink = str


class ActorLayer(str, Enum):
ENTITIES = "rEntitiesLayer"
SOUNDS = "rSoundsLayer"
LIGHTS = "rLightsLayer"


class Brfld(BaseResource):
@classmethod
@functools.lru_cache
def construct_class(cls, target_game: Game) -> construct.Construct:
return standard_format.game_model("CScenario", "49.0.2")

def actors_for_layer(self, name: str) -> dict:
return self.raw.Root.pScenario.rEntitiesLayer.dctSublayers[name].dctActors
@property
def level(self) -> str:
return self.raw.Root.pScenario.sLevelID

@property
def name(self) -> str:
return self.raw.Root.pScenario.sScenarioID

def actors_for_sublayer(self, sublayer_name: str, actor_layer: ActorLayer = ActorLayer.ENTITIES) -> dict:
"""
Gets the actors in a sublayer

param sublayer_name: the name of the sublayer to get the actors of
param actor_layer: the actor_layer the sublayer is in
returns: the actors in the sublayer"""
return self.raw.Root.pScenario[actor_layer].dctSublayers[sublayer_name].dctActors

def sublayers_for_actor_layer(self, actor_layer: ActorLayer = ActorLayer.ENTITIES) -> Iterator[str]:
"""
Iterably gets the names of every sublayer in an actor layer

param actor_layer: the actor layer to get the sublayers of
returns: the name of each sublayer"""
yield from self.raw.Root.pScenario[actor_layer].dctSublayers.keys()

def all_layers(self) -> Iterator[str]:
yield from self.raw.Root.pScenario.rEntitiesLayer.dctSublayers.keys()
def all_actors_in_actor_layer(
self, actor_layer: ActorLayer = ActorLayer.ENTITIES
) -> Iterator[tuple[str, str, construct.Container]]:
"""
Iterably gets every actor in an actor layer

def all_actors(self) -> Iterator[tuple[str, str, construct.Container]]:
for layer_name, sublayer in self.raw.Root.pScenario.rEntitiesLayer.dctSublayers.items():
param actor_layer: the actor layer to get the actors of
returns: for each actor in the actor layer: sublayer name, actor name, actor"""
for sublayer_name, sublayer in self.raw.Root.pScenario[actor_layer].dctSublayers.items():
for actor_name, actor in sublayer.dctActors.items():
yield layer_name, actor_name, actor
yield sublayer_name, actor_name, actor

def follow_link(self, link: BrfldLink) -> Any | None:
"""
Gets the object a link is referencing

def follow_link(self, link: str):
param link: the link to follow
returns: the part of the BRFLD link is referencing"""
if link != "{EMPTY}":
result = self.raw
for part in link.split(":"):
result = result[part]
return result

MayberryZoom marked this conversation as resolved.
Show resolved Hide resolved
def link_for_actor(self, actor_name: str, layer_name: str = "default") -> str:
return ":".join(["Root", "pScenario", "rEntitiesLayer", "dctSublayers", layer_name, "dctActors", actor_name])
def link_for_actor(
self, actor_name: str, sublayer_name: str = "default", actor_layer: ActorLayer = ActorLayer.ENTITIES
) -> BrfldLink:
"""
Builds a link for an actor

def all_actor_groups(self) -> Iterator[str]:
yield from self.raw.Root.pScenario.rEntitiesLayer.dctActorGroups.keys()
param actor_name: the name of the actor
sublayer_name: the name of the sublayer the actor is in
actor_layer: the actor layer the actor is in
returns: a string representing where in the BRFLD the actor is"""
return ":".join(["Root", "pScenario", actor_layer, "dctSublayers", sublayer_name, "dctActors", actor_name])

def get_actor_group(self, group_name: str) -> list[str]:
return self.raw.Root.pScenario.rEntitiesLayer.dctActorGroups[group_name]
def actor_groups_for_actor_layer(self, actor_layer: ActorLayer = ActorLayer.ENTITIES) -> Iterator[str]:
"""
Iterably gets every actor group in an actor layer

def is_actor_in_group(self, group_name: str, actor_name: str, layer_name: str = "default") -> bool:
return self.link_for_actor(actor_name, layer_name) in self.get_actor_group(group_name)
param actor_layer: the actor layer to get the actor groups of
returns: each actor group in the actor layer"""
yield from self.raw.Root.pScenario[actor_layer].dctActorGroups.keys()

def add_actor_to_group(self, group_name: str, actor_name: str, layer_name: str = "default"):
group = self.get_actor_group(group_name)
actor_link = self.link_for_actor(actor_name, layer_name)
def get_actor_group(self, group_name: str, actor_layer: ActorLayer = ActorLayer.ENTITIES) -> list[BrfldLink]:
"""
Gets an actor group

param group_name: the name of the actor group
param actor_layer: the actor layer the actor group is in
returns: a list of links to actors"""
MayberryZoom marked this conversation as resolved.
Show resolved Hide resolved
return self.raw.Root.pScenario[actor_layer].dctActorGroups[group_name]

def is_actor_in_group(
self,
group_name: str,
actor_name: str,
sublayer_name: str = "default",
actor_layer: ActorLayer = ActorLayer.ENTITIES,
) -> bool:
"""
Checks if an actor is in an actor group

param group_name: the name of the actor group
param actor_name: the name of the actor
param sublayer_name: the name of the sublayer the actor is in
param actor_layer: the actor layer the actor is in
returns: true if the actor is in the actor group, false otherwise"""
return self.link_for_actor(actor_name, sublayer_name, actor_layer) in self.get_actor_group(
group_name, actor_layer
)

def add_actor_to_group(
self,
group_name: str,
actor_name: str,
sublayer_name: str = "default",
actor_layer: ActorLayer = ActorLayer.ENTITIES,
) -> None:
"""
Adds an actor to an actor group

param group_name: the name of the actor group
param actor_name: the name of the actor
param sublayer_name: the name of the sublayer the actor is in
param actor_layer: the actor layer the actor is in"""
group = self.get_actor_group(group_name, actor_layer)
actor_link = self.link_for_actor(actor_name, sublayer_name, actor_layer)
if actor_link not in group:
group.append(actor_link)

def remove_actor_from_group(self, group_name: str, actor_name: str, layer_name: str = "default"):
group = self.get_actor_group(group_name)
actor_link = self.link_for_actor(actor_name, layer_name)
else:
raise ValueError(f"Actor {actor_link} is already in actor group {group_name}")

def remove_actor_from_group(
self,
group_name: str,
actor_name: str,
sublayer_name: str = "default",
actor_layer: ActorLayer = ActorLayer.ENTITIES,
) -> None:
"""
Removes an actor from an actor group

param group_name: the name of the actor group
param actor_name: the name of the actor
param sublayer_name: the name of the sublayer the actor is in
param actor_layer: the actor layer the actor is in"""
group = self.get_actor_group(group_name, actor_layer)
actor_link = self.link_for_actor(actor_name, sublayer_name, actor_layer)
if actor_link in group:
group.remove(actor_link)

def add_actor_to_entity_groups(self, collision_camera_name: str, actor_name: str, layer_name: str = "default"):
else:
raise ValueError(f"Actor {actor_link} is not in actor group {group_name}")

def add_actor_to_actor_groups(
self,
collision_camera_name: str,
actor_name: str,
sublayer_name: str = "default",
actor_layer: ActorLayer = ActorLayer.ENTITIES,
) -> None:
"""
adds an actor to all entity groups starting with "eg_" + collision_camera_name
Adds an actor to all actor groups starting with collision_camera_name

param collision_camera_name: name of the collision camera group
(prefix "eg_" is added to find the entity groups)
param actor_name: name of the actor to add to the group
param layer_name: name of the layer the actor belongs to
param collision_camera_name: the name of the collision camera group
param actor_name: the name of the actor to add to the group
param sublayer_name: the name of the sublayer the actor belongs to
param actor_layer: the actor layer the sublayer belongs to
"""
collision_camera_groups = [
group for group in self.all_actor_groups() if group.startswith(f"eg_{collision_camera_name}")
group for group in self.actor_groups_for_actor_layer(actor_layer) if group.startswith(collision_camera_name)
]
for group in collision_camera_groups:
logger.debug("Add actor %s to group %s", actor_name, group)
self.add_actor_to_group(group, actor_name, layer_name)
self.add_actor_to_group(group, actor_name, sublayer_name, actor_layer)
85 changes: 84 additions & 1 deletion tests/formats/test_brfld.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from tests.test_lib import parse_build_compare_editor

from mercury_engine_data_structures import dread_data
from mercury_engine_data_structures.formats.brfld import Brfld
from mercury_engine_data_structures.formats.brfld import ActorLayer, Brfld

bossrush_assets = [
"maps/levels/c10_samus/s201_bossrush_scorpius/s201_bossrush_scorpius.brfld",
Expand All @@ -30,3 +30,86 @@ def test_dread_brfld_100(dread_tree_100, brfld_path):
@pytest.mark.parametrize("brfld_path", bossrush_assets)
def test_dread_brfld_210(dread_tree_210, brfld_path):
parse_build_compare_editor(Brfld, dread_tree_210, brfld_path)


@pytest.mark.parametrize("brfld_path", dread_data.all_files_ending_with(".brfld", bossrush_assets))
def test_get_name(dread_tree_100, brfld_path):
scenario = dread_tree_100.get_file(brfld_path, Brfld)
scenario_name = brfld_path.split("/")[3]

assert scenario.name == scenario_name


@pytest.mark.parametrize("brfld_path", dread_data.all_files_ending_with(".brfld", bossrush_assets))
def test_get_level(dread_tree_100, brfld_path):
scenario = dread_tree_100.get_file(brfld_path, Brfld)
level_name = brfld_path.split("/")[2]

assert scenario.level == level_name


def test_get_actors_methods(dread_tree_100):
scenario = dread_tree_100.get_file("maps/levels/c10_samus/s010_cave/s010_cave.brfld", Brfld)

actors_for_sublayer_names = []

for sublayer in scenario.sublayers_for_actor_layer():
actors_for_sublayer_names += scenario.actors_for_sublayer(sublayer).keys()

all_actors_in_actor_layer_names = [
actor_name for sublayer_name, actor_name, actor in scenario.all_actors_in_actor_layer()
]

assert actors_for_sublayer_names == all_actors_in_actor_layer_names


def test_follow_link(dread_tree_100):
scenario = dread_tree_100.get_file("maps/levels/c10_samus/s010_cave/s010_cave.brfld", Brfld)

actor_link = scenario.link_for_actor("cubemap_fr.2_cave_ini", "cubes", ActorLayer.LIGHTS)

assert scenario.follow_link(actor_link).sName == "cubemap_fr.2_cave_ini"


to_remove_from_actor_groups = [
["eg_collision_camera_000_Default", "breakabletilegroup_052", "breakables", ActorLayer.ENTITIES],
["ssg_collision_camera_000_Default", "Pos_C_Trees_R", "default", ActorLayer.SOUNDS],
["lg_collision_camera_000", "spot_000_1", "cave_000_light", ActorLayer.LIGHTS],
]


@pytest.mark.parametrize(["actor_group", "actor_name", "sublayer_name", "actor_layer"], to_remove_from_actor_groups)
def test_remove_actor_from_actor_group(dread_tree_100, actor_group, actor_name, sublayer_name, actor_layer):
scenario = dread_tree_100.get_file("maps/levels/c10_samus/s010_cave/s010_cave.brfld", Brfld)

scenario.remove_actor_from_group(actor_group, actor_name, sublayer_name, actor_layer)
assert not scenario.is_actor_in_group(actor_group, actor_name, sublayer_name, actor_layer)


def test_remove_actor_from_actor_group_raises_exception(dread_tree_100):
scenario = dread_tree_100.get_file("maps/levels/c10_samus/s010_cave/s010_cave.brfld", Brfld)

with pytest.raises(ValueError, match=r"Actor .+? is not in actor group .+?"):
scenario.remove_actor_from_group("eg_collision_camera_000_Default", "StartPoint0")


to_add_to_actor_groups = [
["eg_collision_camera_000_Default", "breakabletilegroup_000", "breakables", ActorLayer.ENTITIES],
["ssg_collision_camera_000_Default", "Pos_C_LavaWindow_06", "default", ActorLayer.SOUNDS],
["lg_collision_camera_000", "cubemap_006_1_bake", "emmy_006_light", ActorLayer.LIGHTS],
]


@pytest.mark.parametrize(["actor_group", "actor_name", "sublayer_name", "actor_layer"], to_add_to_actor_groups)
def test_add_actor_to_actor_group(dread_tree_100, actor_group, actor_name, sublayer_name, actor_layer):
scenario = dread_tree_100.get_file("maps/levels/c10_samus/s010_cave/s010_cave.brfld", Brfld)

scenario.add_actor_to_actor_groups(actor_group, actor_name, sublayer_name, actor_layer)
assert scenario.is_actor_in_group(actor_group, actor_name, sublayer_name, actor_layer)


def test_add_actor_to_actor_group_raises_exception(dread_tree_100):
scenario = dread_tree_100.get_file("maps/levels/c10_samus/s010_cave/s010_cave.brfld", Brfld)

with pytest.raises(ValueError, match=r"Actor .+? is already in actor group .+?"):
scenario.add_actor_to_group("eg_collision_camera_000_Default", "PRP_DB_CV_006")