diff --git a/src/bl3_mod_menu/options_setup.py b/src/bl3_mod_menu/options_setup.py index 7fb75b1..4d6fcf0 100644 --- a/src/bl3_mod_menu/options_setup.py +++ b/src/bl3_mod_menu/options_setup.py @@ -1,5 +1,5 @@ import functools -from collections.abc import Sequence +from collections.abc import Iterator, Sequence from dataclasses import dataclass from typing import Any @@ -9,6 +9,7 @@ BoolOption, ButtonOption, DropdownOption, + Game, GroupedOption, KeybindOption, Mod, @@ -187,6 +188,53 @@ def get_option_header() -> str: ) +def get_mod_options(mod: Mod) -> tuple[BaseOption, ...]: + """ + Gets the full list of mod options to display, including our custom header. + + Args: + mod: The mod to get the options list of. + Returns: + A tuple of the options to display. + """ + + def inner() -> Iterator[BaseOption]: + # Display the author and version in the title, if they're not the empty string + description_title = "" + if mod.author: + description_title += f"By {mod.author}" + if mod.author and mod.version: + description_title += " - " + if mod.version: + description_title += mod.version + description_title = description_title or "Description" + + description = mod.description + if Game.get_current() not in mod.supported_games: + supported = [g.name for g in Game if g in mod.supported_games] + description = ( + "Incompatible Game!\r" + "This mod supports: " + ", ".join(supported) + "\n\n" + description + ) + + yield ButtonOption( + "Description", + description=description, + description_title=description_title, + ) + + if not mod.enabling_locked: + yield BoolOption( + "Enabled", + mod.is_enabled, + on_change=lambda _, now_enabled: mod.enable() if now_enabled else mod.disable(), + ) + + yield from mod.iter_display_options() + + return tuple(inner()) + + def open_options_menu(main_menu: UObject, mod: Mod) -> None: """ Opens the options menu for a particular mod. @@ -199,7 +247,7 @@ def open_options_menu(main_menu: UObject, mod: Mod) -> None: open_custom_options( main_menu, get_option_header(), - functools.partial(draw_options, options=tuple(mod.iter_display_options()), group_stack=[]), + functools.partial(draw_options, options=get_mod_options(mod), group_stack=[]), ) @@ -220,7 +268,7 @@ def refresh_current_options_menu(options_menu: UObject, preserve_scroll: bool = functools.partial( draw_options, options=( - tuple(option_info.cause.iter_display_options()) + get_mod_options(option_info.cause) if isinstance(option_info.cause, Mod) else option_info.cause.children ), diff --git a/src/console_mod_menu/screens/mod.py b/src/console_mod_menu/screens/mod.py index f253a21..e791f51 100644 --- a/src/console_mod_menu/screens/mod.py +++ b/src/console_mod_menu/screens/mod.py @@ -127,6 +127,28 @@ def __post_init__(self) -> None: def draw(self) -> None: # noqa: D102 draw_stack_header() + header = "" + if self.mod.author: + header += f"By {self.mod.author}" + if self.mod.author and self.mod.version: + header += " - " + if self.mod.version: + header += self.mod.version + draw(header) + + draw(self.mod.get_status()) + draw("") + + if self.mod.description: + draw(self.mod.description) + draw("") + + if not self.mod.enabling_locked: + if self.mod.is_enabled: + draw("[D] Disable") + else: + draw("[E] Enable") + self.drawn_options = [] self.draw_options_list(self.mod.iter_display_options(), []) @@ -136,6 +158,14 @@ def handle_input(self, line: str) -> bool: # noqa: D102 if handle_standard_command_input(line): return True + if not self.mod.enabling_locked: + if self.mod.is_enabled and line.lower() == "d": + self.mod.disable() + return True + if not self.mod.is_enabled and line.lower() == "e": + self.mod.enable() + return True + return self.handle_option_input(line) diff --git a/src/mods_base/mod.py b/src/mods_base/mod.py index 41631fd..7114366 100644 --- a/src/mods_base/mod.py +++ b/src/mods_base/mod.py @@ -14,14 +14,7 @@ from .command import AbstractCommand from .hook import HookProtocol from .keybinds import KeybindType -from .options import ( - BaseOption, - BoolOption, - ButtonOption, - GroupedOption, - KeybindOption, - NestedOption, -) +from .options import BaseOption, GroupedOption, KeybindOption, NestedOption from .settings import default_load_mod_settings, default_save_mod_settings @@ -121,7 +114,9 @@ class Mod: hooks: The mod's hooks. If not given, searches for them in instance variables. commands: The mod's commands. If not given, searches for them in instance variables. - Attributes - Runtime: + Attributes - Enabling: + enabling_locked: If true, the mod cannot be enabled or disabled, it's locked in it's current + state. Set automatically, not available in constructor. is_enabled: True if the mod is currently considered enabled. Not available in constructor. auto_enable: True if to enable the mod on launch if it was also enabled last time. on_enable: A no-arg callback to run on mod enable. Useful when constructing via dataclass. @@ -144,6 +139,7 @@ class Mod: hooks: Sequence[HookProtocol] = field(default=None) # type: ignore commands: Sequence[AbstractCommand] = field(default=None) # type: ignore + enabling_locked: bool = field(init=False) is_enabled: bool = field(default=False, init=False) auto_enable: bool = True on_enable: Callable[[], None] | None = None @@ -194,11 +190,13 @@ def __post_init__(self) -> None: for option in self.options: option.mod = self + self.enabling_locked = Game.get_current() not in self.supported_games + def enable(self) -> None: """Called to enable the mod.""" - if self.is_enabled: + if self.enabling_locked: return - if Game.get_current() not in self.supported_games: + if self.is_enabled: return self.is_enabled = True @@ -224,6 +222,8 @@ def disable(self, dont_update_setting: bool = False) -> None: dont_update_setting: If true, prevents updating the enabled flag in the settings file. Should be set for automated disables, and clear for manual ones. """ + if self.enabling_locked: + return if not self.is_enabled: return @@ -263,39 +263,10 @@ def iter_display_options(self) -> Iterator[BaseOption]: Yields: Options, in the order they should be displayed. """ - compatible_game = Game.get_current() in self.supported_games - - if not compatible_game: - yield ButtonOption( - "Incompatible Game!", - description=f"This mod is incompatible with {Game.get_current().name}!", - ) - - # Displat the author and version in the title, if they're not the empty string - description_title = "" - if self.author: - description_title += f"By {self.author}" - if self.author and self.version: - description_title += " - " - if self.version: - description_title += self.version - - yield ButtonOption( - "Description", - description=self.description, - description_title=description_title or "Description", - ) - if compatible_game: - yield BoolOption( - "Enabled", - self.is_enabled, - on_change=lambda _, now_enabled: self.enable() if now_enabled else self.disable(), - ) - - if any(not opt.is_hidden for opt in self.options) > 0: + if any(not opt.is_hidden for opt in self.options): yield GroupedOption("Options", self.options) - if any(not kb.is_hidden for kb in self.keybinds) > 0: + if any(not kb.is_hidden for kb in self.keybinds): yield GroupedOption( "Keybinds", [KeybindOption.from_keybind(bind) for bind in self.keybinds], @@ -316,21 +287,8 @@ class Library(Mod): def __post_init__(self) -> None: super().__post_init__() - if Game.get_current() in self.supported_games: + # Enable if not already locked due to an incompatible game + if not self.enabling_locked: self.enable() - - def disable(self, dont_update_setting: bool = False) -> None: - """No-op to prevent the library from being disabled.""" - - def iter_display_options(self) -> Iterator[BaseOption]: - """Custom display options, which remove the enabled switch.""" - seen_enabled = False - for option in super().iter_display_options(): - if ( - not seen_enabled - and option.identifier == "Enabled" - and isinstance(option, BoolOption) - ): - seen_enabled = True - continue - yield option + # And then lock + self.enabling_locked = True