Extract plugins from settings.

There isn't really any need for these two types to interact. The lua
plugin manager effectively fully owned its properties, it just delegated
all reads and writes to the settings object.

Instead, break the plugin settings out into the plugin manager and
preserve the manager in the Game. This will make it possible to expose
plugin options in the NGW without breaking the game on cancel.
This commit is contained in:
Dan Albert
2023-04-25 21:25:43 -07:00
parent 664efa3ace
commit 77f1706cbb
16 changed files with 113 additions and 107 deletions

View File

@@ -7,8 +7,6 @@ from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional, TYPE_CHECKING
from game.settings import Settings
if TYPE_CHECKING:
from game.missiongenerator.luagenerator import LuaGenerator
@@ -50,26 +48,10 @@ class PluginSettings:
def __init__(self, identifier: str, enabled_by_default: bool) -> None:
self.identifier = identifier
self.enabled_by_default = enabled_by_default
self.settings = Settings()
self.initialize_settings()
def set_settings(self, settings: Settings) -> None:
self.settings = settings
self.initialize_settings()
def initialize_settings(self) -> None:
# Plugin options are saved in the game's Settings, but it's possible for
# plugins to change across loads. If new plugins are added or new
# options added to those plugins, initialize the new settings.
self.settings.initialize_plugin_option(self.identifier, self.enabled_by_default)
@property
def enabled(self) -> bool:
return self.settings.plugin_option(self.identifier)
self.enabled = enabled_by_default
def set_enabled(self, enabled: bool) -> None:
self.settings.set_plugin_option(self.identifier, enabled)
self.enabled = enabled
class LuaPluginOption(PluginSettings):
@@ -173,6 +155,12 @@ class LuaPlugin(PluginSettings):
def options(self) -> List[LuaPluginOption]:
return self.definition.options
def is_option_enabled(self, identifier: str) -> bool:
for option in self.options:
if option.identifier == identifier:
return option.enabled
raise KeyError(f"Plugin {self.identifier} has no option {self.identifier}")
@classmethod
def from_json(cls, name: str, path: Path) -> Optional[LuaPlugin]:
try:
@@ -183,12 +171,6 @@ class LuaPlugin(PluginSettings):
return cls(definition)
def set_settings(self, settings: Settings) -> None:
"""Attaches the plugin to a settings object."""
super().set_settings(settings)
for option in self.definition.options:
option.set_settings(self.settings)
def inject_scripts(self, lua_generator: LuaGenerator) -> None:
"""Injects the plugin's scripts into the mission."""
for work_order in self.definition.work_orders:
@@ -231,3 +213,11 @@ class LuaPlugin(PluginSettings):
for work_order in self.definition.config_work_orders:
work_order.work(lua_generator)
def update_with(self, other: LuaPlugin) -> None:
self.enabled = other.enabled
for option in self.options:
try:
option.enabled = other.is_option_enabled(option.identifier)
except KeyError:
continue

View File

@@ -1,20 +1,21 @@
from __future__ import annotations
import json
import logging
from collections.abc import Iterator
from pathlib import Path
from typing import Dict, List
from game.settings import Settings
from .luaplugin import LuaPlugin
class LuaPluginManager:
"""Manages available and loaded lua plugins."""
_plugins_loaded = False
_plugins: Dict[str, LuaPlugin] = {}
def __init__(self, plugins: dict[str, LuaPlugin]) -> None:
self._plugins: dict[str, LuaPlugin] = plugins
@classmethod
def _load_plugins(cls) -> None:
@staticmethod
def load() -> LuaPluginManager:
plugins_path = Path("resources/plugins")
path = plugins_path / "plugins.json"
@@ -23,6 +24,7 @@ class LuaPluginManager:
logging.info(f"Reading plugins list from {path}")
plugins = {}
data = json.loads(path.read_text())
for name in data:
plugin_path = plugins_path / name / "plugin.json"
@@ -34,27 +36,41 @@ class LuaPluginManager:
logging.info(f"Loading plugin {name} from {plugin_path}")
plugin = LuaPlugin.from_json(name, plugin_path)
if plugin is not None:
cls._plugins[name] = plugin
cls._plugins_loaded = True
plugins[name] = plugin
return LuaPluginManager(plugins)
@classmethod
def _get_plugins(cls) -> Dict[str, LuaPlugin]:
if not cls._plugins_loaded:
cls._load_plugins()
return cls._plugins
def update_with(self, other: LuaPluginManager) -> None:
"""Updates all setting values with those in the given plugin manager.
@classmethod
def plugins(cls) -> List[LuaPlugin]:
return list(cls._get_plugins().values())
When a game is loaded, LuaPluginManager.load() is called to load the latest set
of plugins and settings. This is called with the plugin manager that was saved
to the Game object to preserve any options that were set, and then the Game is
updated with this manager.
@classmethod
def load_settings(cls, settings: Settings) -> None:
"""Attaches all loaded plugins to the given settings object.
The LuaPluginManager singleton can only be attached to a single Settings object
at a time, and plugins will update the Settings object directly, so attaching
the plugin manager to a detached Settings object (say, during the new game
wizard, but then canceling the new game) will break the settings UI.
This needs to happen because the set of available plugins (or their options) can
change between runs.
"""
for plugin in cls.plugins():
plugin.set_settings(settings)
for plugin in self.iter_plugins():
try:
old_plugin = other.by_id(plugin.identifier)
except KeyError:
continue
plugin.update_with(old_plugin)
def iter_plugins(self) -> Iterator[LuaPlugin]:
yield from self._plugins.values()
def by_id(self, identifier: str) -> LuaPlugin:
return self._plugins[identifier]
def is_plugin_enabled(self, plugin_id: str) -> bool:
try:
return self.by_id(plugin_id).enabled
except KeyError:
return False
def is_option_enabled(self, plugin_id: str, option_id: str) -> bool:
try:
return self.by_id(plugin_id).is_option_enabled(option_id)
except KeyError:
return False