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/.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/CMakeLists.txt b/CMakeLists.txt index 0523038..6963bb8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,9 +1,8 @@ cmake_minimum_required(VERSION 3.24) -project(willow_mod_manager) +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/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" }, { diff --git a/changelog.md b/changelog.md index f1b249b..9b67268 100644 --- a/changelog.md +++ b/changelog.md @@ -7,9 +7,20 @@ ### 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. +> +> - 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 +30,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/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 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] diff --git a/src/legacy_compat/meta_path_finder.py b/src/legacy_compat/meta_path_finder.py index 97c8bf9..ad053e6 100644 --- a/src/legacy_compat/meta_path_finder.py +++ b/src/legacy_compat/meta_path_finder.py @@ -1,12 +1,13 @@ 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 +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. @@ -37,37 +38,32 @@ 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]] - - replacements: Sequence[Pattern] + replacements: SourceReplacement - 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: @@ -105,8 +101,9 @@ def find_spec( # noqa: D102 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( @@ -127,120 +124,10 @@ def find_spec( # noqa: D102 ), ) - # 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):"), - ) - 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 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 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 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.