From 7548b1bb8c9ae98e870ed22db1ab7be6f4128de8 Mon Sep 17 00:00:00 2001 From: apple1417 Date: Sat, 24 May 2025 18:53:12 +1200 Subject: [PATCH 1/7] update inaccurate docs realized this is still copy pasted from oak while copy pasting it for willow1 --- src/ui_utils/hud_message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui_utils/hud_message.py b/src/ui_utils/hud_message.py index 1a1ec1a..fb81a6e 100644 --- a/src/ui_utils/hud_message.py +++ b/src/ui_utils/hud_message.py @@ -9,7 +9,7 @@ def show_hud_message(title: str, msg: str, duration: float = 2.5) -> None: """ Displays a short, non-blocking message in the main in game hud. - Uses the same message style as those for coop players joining/leaving or shift going down. + Uses the same message style as those for respawning. Note this should not be used for critical messages, it may silently fail at any point, and messages may be dropped if multiple are shown too close to each other. From 335a7c520ae814f8a5f3f95fe46dd97108ccbae8 Mon Sep 17 00:00:00 2001 From: apple1417 Date: Sat, 24 May 2025 18:54:34 +1200 Subject: [PATCH 2/7] update several old references to `willow`, to `willow2` --- .cruft.json | 4 ++-- CMakeLists.txt | 2 +- manager_pyproject.toml | 2 +- src/__main__.py | 10 +++++----- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.cruft.json b/.cruft.json index f770fe4..67d5fe7 100644 --- a/.cruft.json +++ b/.cruft.json @@ -11,8 +11,8 @@ "_extensions": [ "cookiecutter.extensions.SlugifyExtension" ], - "project_name": "willow-mod-manager", - "__project_slug": "willow_mod_manager", + "project_name": "willow2-mod-manager", + "__project_slug": "willow2_mod_manager", "include_cpp": true, "include_py": true, "_template": "git@github.com:bl-sdk/common_dotfiles.git", diff --git a/CMakeLists.txt b/CMakeLists.txt index 0523038..217c322 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.24) -project(willow_mod_manager) +project(willow2_mod_manager) set(UNREALSDK_ARCH x86) set(UNREALSDK_UE_VERSION UE3) diff --git a/manager_pyproject.toml b/manager_pyproject.toml index 66e7515..00fc488 100644 --- a/manager_pyproject.toml +++ b/manager_pyproject.toml @@ -4,7 +4,7 @@ # actual releases instead [project] -name = "willow_mod_manager" +name = "willow2_mod_manager" version = "3.7" authors = [{ name = "bl-sdk" }] diff --git a/src/__main__.py b/src/__main__.py index c0ec5a6..d0b3634 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -1,14 +1,14 @@ -# This file is part of the BL2/TPS/AoDK Willow Mod Manager. -# +# This file is part of the BL2/TPS/AoDK Willow2 Mod Manager. +# # -# The Willow Mod Manager is free software: you can redistribute it and/or modify it under the terms +# The Willow2 Mod Manager is free software: you can redistribute it and/or modify it under the terms # of the GNU Lesser General Public License Version 3 as published by the Free Software Foundation. # -# The Willow Mod Manager is distributed in the hope that it will be useful, but WITHOUT ANY +# The Willow2 Mod Manager is distributed in the hope that it will be useful, but WITHOUT ANY # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU Lesser General Public License for more details. # -# You should have received a copy of the GNU General Public License along with the Willow Mod +# You should have received a copy of the GNU General Public License along with the Willow2 Mod # Manager. If not, see . from __future__ import annotations From e525f840182937a0b5df370619d961c273b72936 Mon Sep 17 00:00:00 2001 From: apple1417 Date: Sat, 24 May 2025 20:06:39 +1200 Subject: [PATCH 3/7] move keybinds into submodule --- .gitmodules | 3 ++ src/keybinds | 1 + src/keybinds/__init__.py | 64 ---------------------------------------- 3 files changed, 4 insertions(+), 64 deletions(-) create mode 160000 src/keybinds delete mode 100644 src/keybinds/__init__.py diff --git a/.gitmodules b/.gitmodules index 09d94ad..3b24335 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "src/console_mod_menu"] path = src/console_mod_menu url = ../../bl-sdk/console_mod_menu.git +[submodule "src/keybinds"] + path = src/keybinds + url = ../../bl-sdk/willow_keybinds diff --git a/src/keybinds b/src/keybinds new file mode 160000 index 0000000..beba001 --- /dev/null +++ b/src/keybinds @@ -0,0 +1 @@ +Subproject commit beba001f3591a41690880be6868a3fb5d9b2ec2a diff --git a/src/keybinds/__init__.py b/src/keybinds/__init__.py deleted file mode 100644 index 9369238..0000000 --- a/src/keybinds/__init__.py +++ /dev/null @@ -1,64 +0,0 @@ -from functools import wraps -from typing import TYPE_CHECKING, Any - -from unrealsdk.hooks import Type -from unrealsdk.unreal import BoundFunction, UObject, WrappedStruct - -from mods_base import EInputEvent, KeybindType, hook -from mods_base.mod_list import base_mod - -if TYPE_CHECKING: - from mods_base.keybinds import KeybindCallback_Event, KeybindCallback_NoArgs - -__all__: tuple[str, ...] = ( - "__author__", - "__version__", - "__version_info__", -) - -__version_info__: tuple[int, int] = (1, 1) -__version__: str = f"{__version_info__[0]}.{__version_info__[1]}" -__author__: str = "bl-sdk" - -base_mod.components.append(base_mod.ComponentInfo("Keybinds", __version__)) - -active_keybinds: list[KeybindType] = [] - - -@hook("WillowGame.WillowUIInteraction:InputKey", Type.PRE) -def ui_interaction_input_key( - _1: UObject, - args: WrappedStruct, - _3: Any, - _4: BoundFunction, -) -> None: - key: str = args.Key - event: EInputEvent = args.Event - - for bind in active_keybinds: - if bind.key != key: - continue - if bind.event_filter == event: - callback_no_args: KeybindCallback_NoArgs = bind.callback # type: ignore - callback_no_args() - elif bind.event_filter is None: - callback_event: KeybindCallback_Event = bind.callback # type: ignore - callback_event(event) - - -ui_interaction_input_key.enable() - - -@wraps(KeybindType._enable) # pyright: ignore[reportPrivateUsage] -def enable_keybind(self: KeybindType) -> None: - if self not in active_keybinds: - active_keybinds.append(self) - - -@wraps(KeybindType._disable) # pyright: ignore[reportPrivateUsage] -def disable_keybind(self: KeybindType) -> None: - active_keybinds.remove(self) - - -KeybindType._enable = enable_keybind # pyright: ignore[reportPrivateUsage] -KeybindType._disable = disable_keybind # pyright: ignore[reportPrivateUsage] From 781d29f945d6fd325a274919a3d894e852fa5c80 Mon Sep 17 00:00:00 2001 From: apple1417 Date: Wed, 28 May 2025 19:45:45 +1200 Subject: [PATCH 4/7] pull in submodule updates --- CMakeLists.txt | 3 +-- changelog.md | 35 +++++++++++++++++++++++++++++++++++ libs/pluginloader | 2 +- libs/pyunrealsdk | 2 +- src/mods_base | 2 +- 5 files changed, 39 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 217c322..6963bb8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,8 +2,7 @@ cmake_minimum_required(VERSION 3.24) project(willow2_mod_manager) -set(UNREALSDK_ARCH x86) -set(UNREALSDK_UE_VERSION UE3) +set(UNREALSDK_FLAVOUR WILLOW) set(EXPLICIT_PYTHON_ARCH win32) set(EXPLICIT_PYTHON_VERSION 3.13.1) diff --git a/changelog.md b/changelog.md index f1b249b..a2cf3ec 100644 --- a/changelog.md +++ b/changelog.md @@ -10,6 +10,16 @@ ### [Mods Base v1.10](https://github.com/bl-sdk/mods_base/blob/master/Readme.md#v19) > - Moved a few warnings to go through Python's system, so they get attributed to the right place. +> +> - Added more fixups for Reign of Giants (for real this time). + +### [pyunrealsdk v1.8.0](https://github.com/bl-sdk/pyunrealsdk/blob/master/changelog.md#v180) +> - Trying to overwrite the return value of a void function will now return a more appropriate +> error. +> +> - Upgraded to support unrealsdk v2 - native modules can expect some breakage. The most notable +> effect this has on Python code is a number of formerly read-only fields on core unreal types +> have become read-write. ### Save Options v1.2 - Fixed issue where loading a character with no save option data inherited option values from @@ -19,6 +29,31 @@ ### UI Utils v1.3 - Linting fixes. +### [unrealsdk v2.0.0](https://github.com/bl-sdk/unrealsdk/blob/master/changelog.md#v200) +> - Now supports Borderlands 1. Big thanks to Ry for doing basically all the reverse engineering. +> +> - Major refactor of the core unreal types, to cleanly allow them to change layouts at runtime. All +> core fields have changed from members to zero-arg methods, which return a reference to the +> member. A few classes (e.g. `UProperty` subclasses) previous had existing methods to deal with +> the same problem, these have all been moved to the new system. +> +> Clang is able to detect this change, and gives a nice error recommending inserting brackets at +> the right spot. +> +> - Removed the `UNREALSDK_UE_VERSION` and `UNREALSDK_ARCH` CMake variables, in favour a new merged +> `UNREALSDK_FLAVOUR` variable. +> +> - Removed the (optional) dependency on libfmt, `std::format` support is now required. +> +> - Console commands registered using `unrealsdk::commands::NEXT_LINE` now (try to) only fire on +> direct user input, and ignore commands send via automated means. +> +> - Fixed that assigning an entire array, rather than getting the array and setting it's elements, +> would likely cause memory corruption. This was most common when using an array of large structs, +> and when assigning to one which was previously empty. +> +> - Made `unrealsdk::memory::get_exe_range` public. + ### Willow2 Mod Menu v3.5 - Linting fixups. diff --git a/libs/pluginloader b/libs/pluginloader index f12de3c..1da05ff 160000 --- a/libs/pluginloader +++ b/libs/pluginloader @@ -1 +1 @@ -Subproject commit f12de3cd360536355b2a1c81f00f8fdb44734c44 +Subproject commit 1da05ff095f5454e02466939fe032610f8d5e8c9 diff --git a/libs/pyunrealsdk b/libs/pyunrealsdk index c98c662..d3d415b 160000 --- a/libs/pyunrealsdk +++ b/libs/pyunrealsdk @@ -1 +1 @@ -Subproject commit c98c6625364c86e0d1da7327e6db927f007247c5 +Subproject commit d3d415be6379a450d5a0eb42c80bc86da0f9e3c4 diff --git a/src/mods_base b/src/mods_base index 5757c26..a73104d 160000 --- a/src/mods_base +++ b/src/mods_base @@ -1 +1 @@ -Subproject commit 5757c26d3c8d930f31a84678b95e52a6363cd993 +Subproject commit a73104d4b47bb5a311c3deeaeef6eea097e36ab5 From 9cbb1f4dafc9c7db31c40aa2744b16e3e8cc30c0 Mon Sep 17 00:00:00 2001 From: apple1417 Date: Wed, 28 May 2025 19:53:37 +1200 Subject: [PATCH 5/7] fix reign of giants legacy compat --- changelog.md | 1 + src/legacy_compat/meta_path_finder.py | 28 +++++++++++++++++++++++-- src/legacy_compat/unrealsdk/__init__.py | 2 ++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index a2cf3ec..9b67268 100644 --- a/changelog.md +++ b/changelog.md @@ -7,6 +7,7 @@ ### Legacy Compat v1.6 - Linting fixups. +- Added fixups for Reign of Giants (for real this time). ### [Mods Base v1.10](https://github.com/bl-sdk/mods_base/blob/master/Readme.md#v19) > - Moved a few warnings to go through Python's system, so they get attributed to the right place. diff --git a/src/legacy_compat/meta_path_finder.py b/src/legacy_compat/meta_path_finder.py index 97c8bf9..dfc2817 100644 --- a/src/legacy_compat/meta_path_finder.py +++ b/src/legacy_compat/meta_path_finder.py @@ -7,6 +7,8 @@ from pathlib import Path from types import ModuleType +import unrealsdk + # While just messing with `Mod.__path__` is enough for most most mods, there are a few we need to do # more advanced import hooking on. @@ -37,7 +39,7 @@ def spec_from_string(fullname: str, source: bytes) -> ModuleSpec | None: ) -# Inheriting straight from SourceFileLoade causes some other machinery to expect bytecode? +# Inheriting straight from SourceFileLoader causes some other machinery to expect bytecode? class ReplacementSourceLoader(FileLoader, SourceLoader): # type: ignore type Pattern = tuple[bytes | re.Pattern[bytes], bytes | Callable[[re.Match[bytes]], bytes]] @@ -98,7 +100,7 @@ def get_importing_file() -> Path: raise RuntimeError @classmethod - def find_spec( # noqa: D102 + def find_spec( # noqa: D102, C901 cls, fullname: str, path: Sequence[str] | None = None, @@ -242,5 +244,27 @@ def find_spec( # noqa: D102 (rb"class Hint\(str, enum\.Enum\):", b"class Hint(enum.StrEnum):"), ) + # Reign of Giants is naughty, and deliberately writes an object to a property with a + # different type - so any attempt of doing that in new sdk rightfully throws a type + # error. We need to launder it in now. + # As an extra complication, it turns out if we fix, on older versions of unrealsdk we + # run into a crash - so only apply this replacement if we're running a newer version + case ( + ("src" | "sdk_mods"), + "Mods.ReignOfGiants", + ) if unrealsdk.__version_info__ >= (2, 0, 0): + return spec_with_replacements( + fullname, + path, + target, + ( + rb"(\S+\.DebugPawnMarkerInst) = (.+?)([\r\n])", + rb"_o = \2; _c = _o.Class;" + rb'_o.Class = new_unrealsdk.find_class("MaterialInstanceConstant");' + rb"\1 = _o;" + rb"_o.Class = _c\3", + ), + ) + case _, _: return None diff --git a/src/legacy_compat/unrealsdk/__init__.py b/src/legacy_compat/unrealsdk/__init__.py index febc7f5..6093852 100644 --- a/src/legacy_compat/unrealsdk/__init__.py +++ b/src/legacy_compat/unrealsdk/__init__.py @@ -8,6 +8,7 @@ from functools import cache, wraps from typing import TYPE_CHECKING, Any, overload +import unrealsdk as new_unrealsdk from unrealsdk import ( __version_info__, construct_object, @@ -80,6 +81,7 @@ "UObject", "UPackage", "UStruct", + "new_unrealsdk", ] FStruct = WrappedStruct From 7685195f3c87992792d7c3b825c00d7d8489e608 Mon Sep 17 00:00:00 2001 From: apple1417 Date: Wed, 28 May 2025 20:40:47 +1200 Subject: [PATCH 6/7] refactor source replacements now that we're getting a few, best to put somewhere dedicated and more approachable for others --- src/legacy_compat/meta_path_finder.py | 173 +++-------------------- src/legacy_compat/source_replacements.py | 169 ++++++++++++++++++++++ 2 files changed, 187 insertions(+), 155 deletions(-) create mode 100644 src/legacy_compat/source_replacements.py diff --git a/src/legacy_compat/meta_path_finder.py b/src/legacy_compat/meta_path_finder.py index dfc2817..ad053e6 100644 --- a/src/legacy_compat/meta_path_finder.py +++ b/src/legacy_compat/meta_path_finder.py @@ -1,13 +1,12 @@ import inspect -import re -from collections.abc import Callable, Sequence +from collections.abc import Sequence from importlib.abc import FileLoader, SourceLoader from importlib.machinery import ModuleSpec, PathFinder, SourceFileLoader from importlib.util import spec_from_file_location from pathlib import Path from types import ModuleType -import unrealsdk +from .source_replacements import ALL_SOURCE_REPLACEMENTS, SourceReplacement # While just messing with `Mod.__path__` is enough for most most mods, there are a few we need to do # more advanced import hooking on. @@ -41,35 +40,30 @@ def spec_from_string(fullname: str, source: bytes) -> ModuleSpec | None: # Inheriting straight from SourceFileLoader causes some other machinery to expect bytecode? class ReplacementSourceLoader(FileLoader, SourceLoader): # type: ignore - type Pattern = tuple[bytes | re.Pattern[bytes], bytes | Callable[[re.Match[bytes]], bytes]] + replacements: SourceReplacement - replacements: Sequence[Pattern] - - def __init__(self, fullname: str, path: str, replacements: Sequence[Pattern]) -> None: + def __init__(self, fullname: str, path: str, replacements: SourceReplacement) -> None: super().__init__(fullname, path) self.replacements = replacements def get_data(self, path: str) -> bytes: # noqa: D102 - data = Path(path).read_bytes() - for pattern, replacement in self.replacements: - data = re.sub(pattern, replacement, data) - return data + return self.replacements.apply(path) def spec_with_replacements( fullname: str, - path: Sequence[str] | None = None, - target: ModuleType | None = None, - *replacements: ReplacementSourceLoader.Pattern, + path: Sequence[str] | None, + target: ModuleType | None, + replacements: SourceReplacement, ) -> ModuleSpec | None: """ - Creates a module spec based on the existing module, but applying a set of regex replacements. + Creates a module spec based on the existing module, but with a set of source replacements. Args: fullname: The fully resolved module name, copied from the original call. path: The paths to search, copied from the original call. target: The target copied from the original call. - *replacements: Tuples of a regex pattern and it's replacement. + replacements: The source replacements to apply. """ original_spec = PathFinder.find_spec(fullname, path, target) if original_spec is None or not original_spec.has_location or original_spec.origin is None: @@ -100,15 +94,16 @@ def get_importing_file() -> Path: raise RuntimeError @classmethod - def find_spec( # noqa: D102, C901 + def find_spec( # noqa: D102 cls, fullname: str, path: Sequence[str] | None = None, target: ModuleType | None = None, ) -> ModuleSpec | None: importing_file = cls.get_importing_file() + importing_module_name = importing_file.parent.name - match importing_file.parent.name, fullname: + match importing_module_name, fullname: # EridiumLib adds it's dist folder with a path relative to the executable - fix that case "EridiumLib", "semver": return spec_from_file_location( @@ -129,142 +124,10 @@ def find_spec( # noqa: D102, C901 ), ) - # Constructor has a bad bit of logic for getting the relative file path to execute. - # Since we moved the mods folder, it breaks. Replace it with some proper logic. - case ( - ("Constructor", "Mods.Constructor.hotfix_manager") - | ("Exodus", "Mods.Exodus.hotfix_manager") - | ("ExodusTPS", "Mods.ExodusTPS.hotfix_manager") - | ("Snowbound", "Mods.Snowbound.hotfix_manager") - ): - return spec_with_replacements( - fullname, - path, - target, - # Import on one line just to avoid changing line numbers - (b"import os", b"import os, sys"), - ( - rb'( +)exec_path = str\(file\)\.split\("Binaries\\+", 1\)\[1\]([\r\n]+)' - rb'( +)bl2tools.console_command\("exec " \+ exec_path, False\)([\r\n]+)', - rb"\1exec_path = os.path.relpath(file," - rb' os.path.join(sys.executable, "..", ".."))\2' - rb"\3bl2tools.console_command(f'exec \"{exec_path}\"', False)\4", - ), - ) - - # Imported by the init script, accept both `sdk_mods` folder in the release and the - # `src` folder for if developing in this repo - - # BL2Fix does some redundant random.seed() setting. Py 3.11 removed support for setting - # the seed from an arbitrary type, so just completely get rid of the calls. - case (("src" | "sdk_mods"), "Mods.BL2Fix"): - return spec_with_replacements( - fullname, - path, - target, - (rb"random\.seed\(datetime\.datetime\.now\(\)\)", b""), - ) - - # To help out Text Mod Loader, which does all the actual legacy compat for Arcania, get - # rid of this hook, easier to do here. It ends up permanently enabled otherwise. - case (("src" | "sdk_mods"), "Mods.Arcania"): - return spec_with_replacements( - fullname, - path, - target, - (rb'@ModMenu\.Hook\("Engine\.GameInfo\.PostCommitMapChange"\)', b""), - ) - - # This is a use case the new SDK kind of broke. Reward Rreroller passed the - # `MissionStatusPlayerData:ObjectivesProgress` field to `ShouldGrantAlternateReward`. - # While they're both arrays of ints called `ObjectivesProgress`, since they're different - # properties they're no longer compatible. Turn it into a list to make a copy. - case (("src" | "sdk_mods"), "Mods.RewardReroller"): - return spec_with_replacements( - fullname, - path, - target, - ( - rb"mission\.ShouldGrantAlternateReward\(progress\)", - b"mission.ShouldGrantAlternateReward(list(progress))", - ), - ) - - # Loot randomizer needs quite a few fixes - case ( - # Here's the downside of using a single folder name, these cases just look weird. - ("Mod", "Mods.LootRandomizer.Mod.missions") - # Newer versions split the files - | ("Mod", "Mods.LootRandomizer.Mod.bl2.locations") - | ("Mod", "Mods.LootRandomizer.Mod.tps.locations") - ): - return spec_with_replacements( - fullname, - path, - target, - # It called these functions with a bad arg type in two places. This is - # essentially undefined behaviour, so now gives an exception. Luckily, in this - # case the functions actually validated their arg, so this just became a no-op. - # Remove the two bad calls. - (rb"get_missiontracker\(\)\.(Unr|R)egisterMissionDirector\(giver\)", b"pass"), - # I was just told this one never did anything, idk what the bug is - (rb"sequence\.CustomEnableCondition\.bComplete = True", b""), - # This one's the same as reward reroller, need to manually convert to a list - ( - rb"caller\.FastTravelClip\.SendLocationData\(([\r\n]+ +)" - rb"travels,([\r\n]+ +)" - rb"caller\.LocationStationStrings,", - rb"caller.FastTravelClip.SendLocationData(\1travels,\2list(caller.LocationStationStrings),", - ), - ) - - case ( - "Mod", - "Mods.LootRandomizer.Mod.hints" - | "Mods.LootRandomizer.Mod.bl2" - | "Mods.LootRandomizer.Mod.tps", - ): - return spec_with_replacements( - fullname, - path, - target, - # This is a bit of a weird one. Best we can tell, in legacy sdk if you didn't - # specify a struct field, it just left it alone, so this kept whatever the old - # grades data was. - # In new sdk setting an entire struct zero-inits missing fields instead - so add - # back in what we actually want it to be set to. - ( - rb"inventory_template.Manufacturers = \(\(manufacturer\),\)", - b"inventory_template.Manufacturers = [(manufacturer[0], " - b"[((1, None), (1, 100), (0.5, None, None, 1), (1, None, None, 1))])]", - ), - # This one's a break just due to upgrading python. Hints were trying to be a - # string enum before StrEnum was introduced, so stringifying them now returns - # the name, not the value. Make it a real StrEnum instead. - (rb"class Hint\(str, enum\.Enum\):", b"class Hint(enum.StrEnum):"), - ) - - # Reign of Giants is naughty, and deliberately writes an object to a property with a - # different type - so any attempt of doing that in new sdk rightfully throws a type - # error. We need to launder it in now. - # As an extra complication, it turns out if we fix, on older versions of unrealsdk we - # run into a crash - so only apply this replacement if we're running a newer version - case ( - ("src" | "sdk_mods"), - "Mods.ReignOfGiants", - ) if unrealsdk.__version_info__ >= (2, 0, 0): - return spec_with_replacements( - fullname, - path, - target, - ( - rb"(\S+\.DebugPawnMarkerInst) = (.+?)([\r\n])", - rb"_o = \2; _c = _o.Class;" - rb'_o.Class = new_unrealsdk.find_class("MaterialInstanceConstant");' - rb"\1 = _o;" - rb"_o.Class = _c\3", - ), - ) - case _, _: + # Check for generic source replacements + for entry in ALL_SOURCE_REPLACEMENTS: + if entry.matches(importing_module_name, fullname): + return spec_with_replacements(fullname, path, target, entry) + return None diff --git a/src/legacy_compat/source_replacements.py b/src/legacy_compat/source_replacements.py new file mode 100644 index 0000000..bd52579 --- /dev/null +++ b/src/legacy_compat/source_replacements.py @@ -0,0 +1,169 @@ +from __future__ import annotations + +import re +from collections.abc import Callable +from dataclasses import dataclass +from pathlib import Path +from typing import ClassVar + +import unrealsdk + +ALL_SOURCE_REPLACEMENTS: list[SourceReplacement] = [] + + +@dataclass +class SourceReplacement: + # i.e. things which are valid in calls to re.sub + type Pattern = tuple[bytes | re.Pattern[bytes], bytes | Callable[[re.Match[bytes]], bytes]] + + module_patterns: tuple[tuple[str, str], ...] + replacements: tuple[Pattern, ...] + + auto_register: ClassVar[bool] = True + + def __init__(self, module_patterns: tuple[tuple[str, str], ...], *args: Pattern) -> None: + self.module_patterns = module_patterns + self.replacements = args + + if SourceReplacement.auto_register: + ALL_SOURCE_REPLACEMENTS.append(self) + + def matches(self, importing_module_folder: str, module_name: str) -> bool: + """ + Checks if this matches the given module. + + Args: + importing_module_folder: The folder name which imported this module. + module_name: The full name of this module. + Returns: + True if it matches. + """ + return (importing_module_folder, module_name) in self.module_patterns + + def apply(self, path: str) -> bytes: + """ + Loads a file's bytes with with replacements applied. + + Args: + path: Path to the file to read. + Returns: + The raw file bytes with all replacements applied. + """ + data = Path(path).read_bytes() + for pattern, replacement in self.replacements: + data = re.sub(pattern, replacement, data) + return data + + +# Constructor has a bad bit of logic for getting the relative file path to execute. +# Since we moved the mods folder, it breaks. Replace it with some proper logic. +SourceReplacement( + ( + ("Constructor", "Mods.Constructor.hotfix_manager"), + ("Exodus", "Mods.Exodus.hotfix_manager"), + ("ExodusTPS", "Mods.ExodusTPS.hotfix_manager"), + ("Snowbound", "Mods.Snowbound.hotfix_manager"), + ), + (b"import os", b"import os, sys"), + ( + rb'( +)exec_path = str\(file\)\.split\("Binaries\\+", 1\)\[1\]([\r\n]+)' + rb'( +)bl2tools.console_command\("exec " \+ exec_path, False\)([\r\n]+)', + rb"\1exec_path = os.path.relpath(file," + rb' os.path.join(sys.executable, "..", ".."))\2' + rb"\3bl2tools.console_command(f'exec \"{exec_path}\"', False)\4", + ), +) + +# BL2Fix does some redundant random.seed() setting. Py 3.11 removed support for setting the seed +# from an arbitrary type, so just completely get rid of the calls. +SourceReplacement( + (("src", "Mods.BL2Fix"), ("sdk_mods", "Mods.BL2Fix")), + (rb"random\.seed\(datetime\.datetime\.now\(\)\)", b""), +) + + +# To help out Text Mod Loader, which does all the actual legacy compat for Arcania, get rid of this +# hook, easier to do here. It ends up permanently enabled otherwise. +SourceReplacement( + (("src", "Mods.Arcania"), ("sdk_mods", "Mods.Arcania")), + (rb'@ModMenu\.Hook\("Engine\.GameInfo\.PostCommitMapChange"\)', b""), +) + +# This is a use case the new SDK kind of broke. Reward Reroller passed the +# `MissionStatusPlayerData:ObjectivesProgress` field to `ShouldGrantAlternateReward`. +# While they're both arrays of ints called `ObjectivesProgress`, since they're different properties +# they're no longer compatible. Turn it into a list to make a copy. +SourceReplacement( + (("src", "Mods.RewardReroller"), ("sdk_mods", "Mods.RewardReroller")), + ( + rb"mission\.ShouldGrantAlternateReward\(progress\)", + b"mission.ShouldGrantAlternateReward(list(progress))", + ), +) + +# Loot randomizer needs quite a few fixes +SourceReplacement( + ( + # Here's the downside of using a single folder name, these cases just look weird. + ("Mod", "Mods.LootRandomizer.Mod.missions"), + # Newer versions split the files + ("Mod", "Mods.LootRandomizer.Mod.bl2.locations"), + ("Mod", "Mods.LootRandomizer.Mod.tps.locations"), + ), + # It called these functions with a bad arg type in two places. This is essentially undefined + # behaviour, so now gives an exception. Luckily, in this case the functions actually validated + # their arg, so this just became a no-op. + # Remove the two bad calls. + (rb"get_missiontracker\(\)\.(Unr|R)egisterMissionDirector\(giver\)", b"pass"), + # I was just told this one never did anything, idk what the bug is + (rb"sequence\.CustomEnableCondition\.bComplete = True", b""), + # This one's the same as reward reroller, need to manually convert to a list + ( + rb"caller\.FastTravelClip\.SendLocationData\(([\r\n]+ +)" + rb"travels,([\r\n]+ +)" + rb"caller\.LocationStationStrings,", + rb"caller.FastTravelClip.SendLocationData(\1travels,\2list(caller.LocationStationStrings),", + ), +) + +SourceReplacement( + ( + ("Mod", "Mods.LootRandomizer.Mod.hints"), + ("Mod", "Mods.LootRandomizer.Mod.bl2"), + ("Mod", "Mods.LootRandomizer.Mod.tps"), + ), + # This is a bit of a weird one. Best we can tell, in legacy sdk if you didn't specify a struct + # field, it just left it alone, so this kept whatever the old grades data was. + # In new sdk setting an entire struct zero-inits missing fields instead - so add back in what we + # actually want it to be set to. + ( + rb"inventory_template.Manufacturers = \(\(manufacturer\),\)", + b"inventory_template.Manufacturers = [(manufacturer[0], " + b"[((1, None), (1, 100), (0.5, None, None, 1), (1, None, None, 1))])]", + ), + # This one's a break just due to upgrading python. Hints were trying to be a string enum before + # StrEnum was introduced, so stringifying them now returns the name, not the value. Make it a + # real StrEnum instead. + (rb"class Hint\(str, enum\.Enum\):", b"class Hint(enum.StrEnum):"), +) + + +# It turns out, the error here is protecting us from a full game crash on older versions of +# unrealsdk, so only apply this fix if we're running a new enough one +if unrealsdk.__version_info__ >= (2, 0, 0): + # Reign of Giants is naughty, and deliberately writes an object to a property with a different + # type - so any attempt of doing that in new sdk rightfully throws a type error. We need to + # launder it in now. + SourceReplacement( + (("src", "Mods.ReignOfGiants"), ("sdk_mods", "Mods.ReignOfGiants")), + ( + rb"(\S+\.DebugPawnMarkerInst) = (.+?)([\r\n])", + rb"_o = \2; _c = _o.Class;" + rb'_o.Class = new_unrealsdk.find_class("MaterialInstanceConstant");' + rb"\1 = _o;" + rb"_o.Class = _c\3", + ), + ) + + +SourceReplacement.auto_register = False From 4322502e8675dd6c6a7ccfc8915de7cd0d51c623 Mon Sep 17 00:00:00 2001 From: apple1417 Date: Wed, 28 May 2025 21:10:23 +1200 Subject: [PATCH 7/7] fixup cmake presets --- CMakePresets.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CMakePresets.json b/CMakePresets.json index fb46af1..e31e154 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -30,9 +30,10 @@ "lhs": "${hostSystemName}", "rhs": "Windows" }, - "environment": { + "cacheVariables": { "MSVC_WINE_ENV_SCRIPT": "/win-sdk/bin/x86/msvcenv.sh" }, + "generator": "Ninja", "toolchainFile": "libs/pyunrealsdk/common_cmake/clang-cross-x86.cmake" }, { @@ -43,6 +44,7 @@ "lhs": "${hostSystemName}", "rhs": "Windows" }, + "generator": "Ninja", "toolchainFile": "libs/pyunrealsdk/common_cmake/i686-w64-mingw32.cmake" }, { @@ -53,6 +55,7 @@ "lhs": "${hostSystemName}", "rhs": "Windows" }, + "generator": "Ninja", "toolchainFile": "libs/pyunrealsdk/common_cmake/llvm-i686-w64-mingw32.cmake" }, {