diff --git a/text_mod_loader/Readme.md b/text_mod_loader/Readme.md index b8288a6..7c7486f 100644 --- a/text_mod_loader/Readme.md +++ b/text_mod_loader/Readme.md @@ -27,6 +27,9 @@ Text Mod Loader supports the following tags. # Changelog +## Text Mod Loader v4 +- Added legacy compat for Arcania, which it turns out did actually use `add_custom_mod_path`. + ## Text Mod Loader v3 - Fixed that the settings file would corrupt every other launch, losing your auto enable settings. diff --git a/text_mod_loader/__init__.py b/text_mod_loader/__init__.py index e0fb68d..ce761eb 100644 --- a/text_mod_loader/__init__.py +++ b/text_mod_loader/__init__.py @@ -5,8 +5,10 @@ from typing import Any +from legacy_compat import add_compat_module from mods_base import ButtonOption, Library, build_mod, hook +from . import legacy_compat as tml_legacy_compat from .loader import all_text_mods, load_all_text_mods from .settings import ( all_settings, @@ -46,3 +48,5 @@ def reload(_: ButtonOption) -> None: ) sanitize_settings() load_all_text_mods() + +add_compat_module("Mods.TextModLoader", tml_legacy_compat) diff --git a/text_mod_loader/legacy_compat.py b/text_mod_loader/legacy_compat.py new file mode 100644 index 0000000..a145f00 --- /dev/null +++ b/text_mod_loader/legacy_compat.py @@ -0,0 +1,73 @@ +from collections.abc import Callable +from pathlib import Path +from typing import Any + +from legacy_compat import legacy_compat +from mods_base import hook, register_mod +from unrealsdk.hooks import Block +from unrealsdk.unreal import BoundFunction, UFunction, UObject, WrappedStruct + +from .anti_circular_import import all_text_mods +from .loader import load_mod_info +from .settings import get_cached_mod_info, update_cached_mod_info +from .text_mod import TextMod as NewTextMod + +# The old TML Python interface was a very leaky abstraction. Our internals don't really match up +# with it anymore. +# Luckily, it seems there's only actually one mod which used it, Arcania. We can just create a fake +# interface to catch it specifically. + +__all__: tuple[str, ...] = ( + "TextMod", + "add_custom_mod_path", +) + + +class TextMod: + Name: str + Author: str + Description: str + Version: str + + onLevelTransition: Callable[[UObject, UFunction, WrappedStruct], bool] # noqa: N815 + + +def add_custom_mod_path(filename: str, cls: type[TextMod] = TextMod) -> None: # noqa: D103 + if not ((path := Path(filename)).name == "Arcania.blcm" and cls.__name__ == "Arcania"): + raise RuntimeError(f"Text Mod Loader legacy compat not implemented for {path.name}") + + if (mod_info := get_cached_mod_info(path)) is None: + mod_info = load_mod_info(path) + + mod_info["title"] = cls.Name + mod_info["author"] = cls.Author + mod_info["description"] = cls.Description + mod_info["version"] = cls.Version + + update_cached_mod_info(path, mod_info) + + @hook("Engine.GameInfo:PostCommitMapChange") + def on_level_transition( + obj: UObject, + args: WrappedStruct, + _3: Any, + func: BoundFunction, + ) -> type[Block] | None: + with legacy_compat(): + ret = cls.onLevelTransition(obj, func.func, args) + return Block if ret else None + + mod = NewTextMod( + name=mod_info["title"], + author=mod_info["author"], + version=mod_info["version"], + file=path, + spark_service_idx=mod_info["spark_service_idx"], + recommended_game=mod_info["recommended_game"], + internal_description=mod_info["description"], + prevent_reloading=True, + hooks=(on_level_transition,), + ) + + all_text_mods[path] = mod + register_mod(mod) diff --git a/text_mod_loader/loader.py b/text_mod_loader/loader.py index c75db18..5f712e2 100644 --- a/text_mod_loader/loader.py +++ b/text_mod_loader/loader.py @@ -165,6 +165,9 @@ def load_all_text_mods() -> None: for mod in list(all_text_mods.values()): mod.check_deleted() + if mod.prevent_reloading: + continue + match mod.state: # Delete what mods we can case ( diff --git a/text_mod_loader/pyproject.toml b/text_mod_loader/pyproject.toml index 180dc6d..3a384c0 100644 --- a/text_mod_loader/pyproject.toml +++ b/text_mod_loader/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "text_mod_loader" -version = "3" +version = "4" authors = [{ name = "apple1417" }] description = """\ Displays Text Mods from binaries in the SDK mods menu. diff --git a/text_mod_loader/text_mod.py b/text_mod_loader/text_mod.py index beacb7b..2d8ecc6 100644 --- a/text_mod_loader/text_mod.py +++ b/text_mod_loader/text_mod.py @@ -1,20 +1,20 @@ from __future__ import annotations +import os +import sys from dataclasses import KW_ONLY, dataclass -from typing import TYPE_CHECKING, Literal +from pathlib import Path +from typing import Literal from mods_base import Game, Mod, get_pc - -from .anti_circular_import import TextModState - -if TYPE_CHECKING: - from pathlib import Path - from ui_utils import TrainingBox +from .anti_circular_import import TextModState from .hotfixes import any_hotfix_used, is_hotfix_service from .settings import change_mod_auto_enable +BINARIES_DIR = Path(sys.executable).parent.parent + @dataclass class TextMod(Mod): @@ -24,6 +24,7 @@ class TextMod(Mod): file: Path state: TextModState = TextModState.Disabled + prevent_reloading: bool = False spark_service_idx: int | None recommended_game: Game | None @@ -127,7 +128,10 @@ def enable(self) -> None: # noqa: D102 return case TextModState.Disabled: - get_pc().ConsoleCommand(f'exec "{self.file}"') + # Path.relative_to requires one path be a subpath of the other, it won't prefix + # `../`s if we're executing something in a parent dir of binaries + get_pc().ConsoleCommand(f'exec "{os.path.relpath(self.file, BINARIES_DIR)}"') + self.state = TextModState.Enabled change_mod_auto_enable(self, True) case TextModState.DisableOnRestart: