Skip to content

Commit

Permalink
Merge pull request #382 from MayberryZoom/mass-delete-actors
Browse files Browse the repository at this point in the history
Add JSON API for mass deleting actors
  • Loading branch information
henriquegemignani authored Dec 23, 2024
2 parents e6a7f8f + fafe819 commit 96850d5
Show file tree
Hide file tree
Showing 7 changed files with 293 additions and 5 deletions.
13 changes: 8 additions & 5 deletions src/open_dread_rando/door_locks/door_patcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,8 @@ def rename_shields(self, door: Container, scenario: str):
continue

# get shield actor and cache its sName
shieldActor = self.editor.resolve_actor_reference(self.editor.reference_for_link(link, scenario))
reference = self.editor.reference_for_link(link, scenario)
shieldActor = self.editor.resolve_actor_reference(reference)
old_sName = shieldActor.sName

# skip hdoors (doors where the environment covers one side of the door)
Expand All @@ -411,16 +412,18 @@ def rename_shields(self, door: Container, scenario: str):
# reclaim old shield id if this is a RandoShield
self.reclaim_old_shield_id(shieldActor.sName, scenario)

# grab the lowest open id and rename it
# grab the lowest open id
new_id = self.get_shield_id(scenario)
shieldActor.sName = new_id
life_comp[link_name] = self.editor.build_link(new_id)

# make new actor, copy its groups, delete it
brfld = self.editor.get_scenario(scenario)
brfld.actors_for_sublayer('default')[new_id] = shieldActor
self.editor.copy_actor_groups({ "actor": old_sName }, { "actor": new_id }, scenario)
brfld.actors_for_sublayer('default').pop(old_sName)
self.editor.remove_entity(reference, None)

# actually rename it
shieldActor.sName = new_id
life_comp[link_name] = self.editor.build_link(new_id)

# update the minimap entry as well
mapBlockages = self.editor.get_scenario_map(scenario).raw.Root.mapBlockages
Expand Down
4 changes: 4 additions & 0 deletions src/open_dread_rando/dread_patcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from open_dread_rando.pickups.split_pickups import patch_split_pickups, update_starting_inventory_split_pickups
from open_dread_rando.specific_patches import game_patches
from open_dread_rando.specific_patches.environmental_damage import apply_constant_damage
from open_dread_rando.specific_patches.mass_delete_actors import mass_delete_actors
from open_dread_rando.specific_patches.objective import apply_objective_patches
from open_dread_rando.specific_patches.static_fixes import apply_static_fixes
from open_dread_rando.validator_with_default import DefaultValidatingDraft7Validator
Expand Down Expand Up @@ -271,6 +272,9 @@ def apply_patches(editor: PatcherEditor, lua_editor: LuaEditor, configuration: d
# Specific game patches
game_patches.apply_game_patches(editor, configuration.get("game_patches", {}))

# Mass delete actors
mass_delete_actors(editor, configuration["mass_delete_actors"])

# Actor patches
apply_actor_patches(editor, configuration.get("actor_patches"))

Expand Down
98 changes: 98 additions & 0 deletions src/open_dread_rando/files/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,104 @@
"additionalProperties": false,
"default": {}
},
"mass_delete_actors": {
"description": "Deletes actors en masse",
"type": "object",
"properties": {
"to_remove": {
"type": "array",
"items": {
"examples": [
{
"scenario": "s010_cave",
"actor_layer": "rLightsLayer",
"method": "all"
},
{
"scenario": "s010_cave",
"method": "remove_from_groups",
"actor_groups": [
"eg_collision_camera_067_Default"
]
},
{
"scenario": "s030_baselab",
"actor_layer": "rLightsLayer",
"method": "keep_from_groups",
"actor_groups": [
"lg_collision_camera_011_Default"
]
}
],
"type": "object",
"properties": {
"scenario": {
"description": "The scenario to remove actors from",
"$ref": "#/$defs/scenario_name"
},
"actor_layer": {
"description": "The actor layer to remove actors from",
"type": "string",
"enum": [
"rEntitiesLayer",
"rSoundsLayer",
"rLightsLayer"
],
"default": "rEntitiesLayer"
},
"method": {
"description": "The method for removing actors. all removes all in the scenario, remove_from_groups will remove all actors in the provided actor groups, and keep_from_groups will remove from all actor groups not provided",
"type": "string",
"enum": [
"all",
"remove_from_groups",
"keep_from_groups"
],
"default": "all"
},
"actor_groups": {
"description": "The actor group the actors are in. Required if method is not all",
"type": "array",
"items": {
"type": "string"
}
}
},
"required": ["scenario"],
"if": {
"anyOf": [
{
"properties": {
"method": { "const": "remove_from_groups" }
},
"required": ["method"]
},
{
"properties": {
"method": { "const": "keep_from_groups" }
},
"required": ["method"]
}
]
},
"then": {
"required": ["actor_groups"]
}
},
"default": []
},
"to_keep": {
"description": "A list of actors not to remove. Use this to keep specific actors from a scenario or actor group that has been removed",
"type": "array",
"items": {
"$ref": "#/$defs/actor_reference_with_layer"
},
"default": []
}
},
"required": ["to_remove"],
"default": {}
},
"show_shields_on_minimap": {
"type": "boolean",
"description": "Deprecated. Used to remove shields from the minimaps in Door Lock Rando.",
Expand Down
85 changes: 85 additions & 0 deletions src/open_dread_rando/specific_patches/mass_delete_actors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from __future__ import annotations

import typing

from open_dread_rando.patcher_editor import PatcherEditor


class ActorReferenceTuple(typing.NamedTuple):
scenario: str
actor_layer: str
sublayer: str
actor: str

def from_dict(reference: dict[str, str]) -> ActorReferenceTuple:
return ActorReferenceTuple(
reference["scenario"],
reference["actor_layer"],
reference.get("sublayer", reference.get("layer")),
reference["actor"]
)

def _remove_all_actors(editor: PatcherEditor, scenario_name: str, actor_layer: str) -> set[ActorReferenceTuple]:
to_remove = set()
scenario = editor.get_scenario(scenario_name)

for sublayer_name, actor_name, actor in scenario.all_actors_in_actor_layer(actor_layer):
to_remove.add(ActorReferenceTuple(scenario_name, actor_layer, sublayer_name, actor_name))

return to_remove

def _remove_actors_from_groups(editor: PatcherEditor, scenario_name: str, actor_layer: str,
actor_groups: list[str]) -> set[ActorReferenceTuple]:
to_remove = set()
scenario = editor.get_scenario(scenario_name)

for group in actor_groups:
for actor_link in scenario.get_actor_group(group, actor_layer):
to_remove.add(ActorReferenceTuple(**editor.reference_for_link(actor_link, scenario_name)))

return to_remove

def _remove_actors_not_in_groups(editor: PatcherEditor, scenario_name: str, actor_layer: str,
actor_groups: list[str]) -> tuple[set[ActorReferenceTuple], set[ActorReferenceTuple]]:
to_remove = set()
to_keep = set()
scenario = editor.get_scenario(scenario_name)

for group in scenario.actor_groups_for_actor_layer(actor_layer):
if group not in actor_groups:
for actor_link in scenario.get_actor_group(group, actor_layer):
to_remove.add(ActorReferenceTuple(**editor.reference_for_link(actor_link, scenario_name)))
else:
for actor_link in scenario.get_actor_group(group, actor_layer):
to_keep.add(ActorReferenceTuple(**editor.reference_for_link(actor_link, scenario_name)))

return to_remove, to_keep

def mass_delete_actors(editor: PatcherEditor, configuration: dict) -> None:
# Sets of tuples will be used to ensure no duplicate entries
to_remove = set()
to_keep = { ActorReferenceTuple.from_dict(reference) for reference in configuration["to_keep"] }

for scenario_config in configuration["to_remove"]:
scenario_name = scenario_config["scenario"]
actor_layer = scenario_config["actor_layer"]
method = scenario_config["method"]

if method == "all":
to_remove.update(_remove_all_actors(editor, scenario_name, actor_layer))

elif method == "remove_from_groups":
to_remove.update(_remove_actors_from_groups(editor, scenario_name, actor_layer,
scenario_config["actor_groups"]))

elif method == "keep_from_groups":
new_remove, new_keep = _remove_actors_not_in_groups(editor, scenario_name, actor_layer,
scenario_config["actor_groups"])

to_remove.update(new_remove)
to_keep.update(new_keep)

to_remove.difference_update(to_keep)

for actor in to_remove:
editor.remove_entity(actor._asdict(), None)
7 changes: 7 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import lupa
import pytest

from open_dread_rando.patcher_editor import PatcherEditor

_FAIL_INSTEAD_OF_SKIP = True


Expand Down Expand Up @@ -56,6 +58,11 @@ def lua_runtime():
return runtime


@pytest.fixture()
def patcher_editor(dread_path):
return PatcherEditor(dread_path)


def pytest_addoption(parser):
parser.addoption('--skip-if-missing', action='store_false', dest="fail_if_missing",
default=True, help="Skip tests instead of missing, in case any asset is missing")
Expand Down
59 changes: 59 additions & 0 deletions tests/test_dread_patcher.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from unittest.mock import MagicMock

from mercury_engine_data_structures.formats.brfld import ActorLayer

from open_dread_rando import dread_patcher
from open_dread_rando.specific_patches.mass_delete_actors import mass_delete_actors


def test_cosmetic_options(lua_runtime):
Expand Down Expand Up @@ -66,3 +69,59 @@ def test_cosmetic_options(lua_runtime):

assert lua_runtime.eval("Init.fEnergyPerTank") == 75
assert lua_runtime.eval("Init.sLayoutUUID") == layoutUUID

def test_mass_delete_actors(patcher_editor):
configuration = {
"to_remove": [
{
"scenario": "s020_magma",
"actor_layer": "rEntitiesLayer",
"method": "all"
},
{
"scenario": "s010_cave",
"actor_layer": "rLightsLayer",
"method": "remove_from_groups",
"actor_groups": [
"lg_collision_camera_001"
]
},
{
"scenario": "s030_baselab",
"actor_layer": "rLightsLayer",
"method": "keep_from_groups",
"actor_groups": [
"lg_collision_camera_011_Default"
]
}
],
"to_keep": [
{
"scenario": "s010_cave",
"actor_layer": "rLightsLayer",
"sublayer": "cave_001_light",
"actor": "spot_001_1"
}
]
}

mass_delete_actors(patcher_editor, configuration)

s010_cave = patcher_editor.get_scenario("s010_cave")
cave_light_001 = s010_cave.get_actor_group("lg_collision_camera_001", "rLightsLayer")
cave_spot_001_1_link = patcher_editor.build_link("spot_001_1", "cave_001_light", ActorLayer.LIGHTS)

assert cave_light_001 == [cave_spot_001_1_link]

s020_magma = patcher_editor.get_scenario("s020_magma")
magma_entities = [actor_name for (sublayer_name, actor_name, actor) in s020_magma.all_actors_in_actor_layer()]

assert len(magma_entities) == 0

s030_baselab = patcher_editor.get_scenario("s030_baselab")
lab_light_001 = s030_baselab.get_actor_group("lg_collision_camera_001_Default", "rLightsLayer")
lab_light_010 = s030_baselab.get_actor_group("lg_collision_camera_010_Default", "rLightsLayer")
lab_light_011 = s030_baselab.get_actor_group("lg_collision_camera_011_Default", "rLightsLayer")
cubemap_010_link = patcher_editor.build_link("cubemap_010", "base_010_light", ActorLayer.LIGHTS)

assert len(lab_light_011) == 3 and len(lab_light_001) == 0 and cubemap_010_link in lab_light_010
32 changes: 32 additions & 0 deletions tests/test_files/patcher_files/april_fools_patcher.json
Original file line number Diff line number Diff line change
Expand Up @@ -7448,6 +7448,38 @@
"{c1}Metroid DNA 4{c0} is guarded by {c2}E.M.M.I.-07PB{c0}."
]
},
"mass_delete_actors": {
"to_remove": [
{
"scenario": "s020_magma",
"method": "all"
},
{
"scenario": "s010_cave",
"actor_layer": "rLightsLayer",
"method": "remove_from_groups",
"actor_groups": [
"lg_collision_camera_001"
]
},
{
"scenario": "s030_baselab",
"actor_layer": "rLightsLayer",
"method": "keep_from_groups",
"actor_groups": [
"lg_collision_camera_011_Default"
]
}
],
"to_keep": [
{
"scenario": "s010_cave",
"actor_layer": "rLightsLayer",
"sublayer": "cave_001_light",
"actor": "spot_001_1"
}
]
},
"layout_uuid": "00000000-0000-1111-0000-000000000000",
"mod_compatibility": "ryujinx",
"mod_category": "romfs"
Expand Down

0 comments on commit 96850d5

Please sign in to comment.