diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 5ac9aba..ec3f501 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -105,7 +105,7 @@ jobs: - name: Run Tests run: - venv/bin/python -m pytest --cov src --cov-report=xml --durations=100 + venv/bin/python -m pytest --cov src --cov-report=xml --durations=100 -n 4 - name: codecov uses: codecov/codecov-action@v4 diff --git a/pyproject.toml b/pyproject.toml index 9c9b37f..8c8da05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ requires-python = ">=3.10" dynamic = ["version"] dependencies = [ - "retro-data-structures>=0.23.0", + "retro-data-structures>=0.28.0", "jsonschema>=4.0.0", "ppc-asm", "py_randomprime", # for Prime 1 symbols @@ -37,6 +37,7 @@ test = [ "pytest", "pytest-cov", "pytest-mock", + "pytest-xdist", "pre-commit", ] diff --git a/src/open_prime_rando/echoes/schema.json b/src/open_prime_rando/echoes/schema.json index 6decb11..7c1d443 100644 --- a/src/open_prime_rando/echoes/schema.json +++ b/src/open_prime_rando/echoes/schema.json @@ -97,6 +97,44 @@ "required": [ "suits" ] + }, + "tweaks": { + "type": "object", + "description": "Allows arbitrary changes to the tweaks", + "propertyNames": { + "enum": [ + "TweakGui", + "TweakTargeting", + "TweakPlayerRes", + "TweakPlayerControls2", + "TweakParticle", + "TweakGuiColors", + "TweakGame", + "TweakPlayer2", + "TweakSlideShow", + "TweakBall", + "TweakAutoMapper", + "TweakPlayerControls", + "TweakPlayerGunMuli", + "TweakPlayerGun", + "TweakCameraBob", + "TweakPlayer" + ] + }, + "additionalProperties": { + "type": "object", + "description": "Mapping of full property path to new value. For nested properties, include parent property names split with .", + "additionalProperties": true + }, + "examples": [ + { + "TweakPlayer": { + "collision.ball_radius": 0.5, + "dark_world.damage_per_second.di_damage": 1, + "dark_world.unknown_0x19275a97": 0.5 + } + } + ] } }, "required": [ diff --git a/src/open_prime_rando/echoes_patcher.py b/src/open_prime_rando/echoes_patcher.py index c1a9eab..aed84af 100644 --- a/src/open_prime_rando/echoes_patcher.py +++ b/src/open_prime_rando/echoes_patcher.py @@ -1,5 +1,6 @@ import json import logging +import typing from collections.abc import Callable from pathlib import Path from typing import TYPE_CHECKING @@ -124,6 +125,30 @@ def apply_corrupted_memory_card_change(editor: PatcherEditor): table.set_string(name_to_index["ChoiceDeleteCorruptedFile"], "Delete Incompatible File") +def apply_tweak_edits(editor: PatcherEditor, tweak_edits: dict[str, dict[str, typing.Any]]) -> None: + """ + Edits the tweaks based on the generic schema api + :param editor: + :param tweak_edits: + :return: + """ + for instance in editor.tweaks.instances: + properties = instance.get_properties().to_json() + if properties["instance_name"] in tweak_edits: + logging.debug("Editing %s", properties["instance_name"]) + + for name, value in tweak_edits[properties["instance_name"]].items(): + parent = properties + spit_name = name.split(".") + + for part in spit_name[:-1]: + parent = parent[part] + + parent[spit_name[-1]] = value + + instance.set_properties(instance.type.from_json(properties)) + + def patch_paks( file_provider: FileProvider, output_path: Path, @@ -155,6 +180,10 @@ def patch_paks( apply_small_randomizations(editor, configuration["small_randomizations"]) apply_corrupted_memory_card_change(editor) + if "tweaks" in configuration: + status_update("Modifying tweaks", 0) + apply_tweak_edits(editor, configuration["tweaks"]) + status_update("Modifying areas", 0) apply_area_modifications(editor, configuration["worlds"], status_update) diff --git a/src/open_prime_rando/patcher_editor.py b/src/open_prime_rando/patcher_editor.py index 36b2146..090c01d 100644 --- a/src/open_prime_rando/patcher_editor.py +++ b/src/open_prime_rando/patcher_editor.py @@ -9,6 +9,7 @@ from retro_data_structures.crc import crc32 from retro_data_structures.formats.mlvl import Mlvl from retro_data_structures.formats.mrea import Area +from retro_data_structures.formats.ntwk import Ntwk from retro_data_structures.formats.strg import Strg from retro_data_structures.game_check import Game @@ -31,6 +32,8 @@ def _seek_and_write(self, seek: int, data: bytes): class PatcherEditor(AssetManager): memory_files: dict[NameOrAssetId, BaseResource] + dol: MemoryDol | None = None + tweaks: Ntwk | None = None def __init__(self, provider: FileProvider, game: Game): super().__init__(provider, game) @@ -38,8 +41,9 @@ def __init__(self, provider: FileProvider, game: Game): if game in [Game.PRIME, Game.ECHOES]: self.dol = MemoryDol(provider.get_dol()) - else: - self.dol = None + if game == Game.ECHOES: + with provider.open_binary("Standard.ntwk") as f: + self.tweaks = Ntwk.parse(f.read(), game) def get_file(self, path: NameOrAssetId, type_hint: type[T] = BaseResource) -> T: if path not in self.memory_files: @@ -79,6 +83,9 @@ def save_modifications(self, output_path: Path): target_dol.parent.mkdir(exist_ok=True, parents=True) target_dol.write_bytes(self.dol.dol_file.getvalue()) + if self.tweaks is not None: + output_path.joinpath("files/Standard.ntwk").write_bytes(self.tweaks.build()) + def add_or_replace_custom_asset(self, name: str, new_data: Resource) -> AssetId: if self.does_asset_exists(name): asset_id = self.replace_asset(name, new_data) diff --git a/tests/test_files/echoes/door_lock.json b/tests/test_files/echoes/door_lock.json index a922d89..d95b766 100644 --- a/tests/test_files/echoes/door_lock.json +++ b/tests/test_files/echoes/door_lock.json @@ -3326,5 +3326,11 @@ "dark": "player2", "light": "player3" } + }, + "tweaks": { + "TweakPlayer": { + "dark_world.damage_per_second.di_damage": 1, + "dark_world.unknown_0x19275a97": 0.5 + } } } \ No newline at end of file