From 8827f7df345afa2a0bf297a9043335baa1334e7f Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 24 Oct 2020 00:34:23 -0700 Subject: [PATCH] Cleanup Lua plugin implementation. * Move the UI code out of the plugin logic. * Add types where needed. * Move into game package. * Improve error handling. * Simplify settings behavior. * Don't load disabled plugins. * Remove knowledge of non-base plugins from game generation. Fixes https://github.com/Khopa/dcs_liberation/issues/311 --- game/game.py | 9 +- game/operation/operation.py | 9 +- game/plugins/__init__.py | 2 + game/plugins/luaplugin.py | 180 ++++++++++++++++++ game/plugins/manager.py | 50 +++++ game/settings.py | 24 ++- gen/armor.py | 6 +- plugin/__init__.py | 2 - plugin/luaplugin.py | 208 --------------------- plugin/manager.py | 43 ----- qt_ui/windows/settings/QSettingsWindow.py | 61 ++---- qt_ui/windows/settings/plugins.py | 71 +++++++ resources/plugins/_doc/plugins_readme.md | 30 +-- resources/plugins/base/plugin.json | 1 - resources/plugins/jtacautolase/plugin.json | 1 - resources/plugins/skynetiads/plugin.json | 1 - 16 files changed, 357 insertions(+), 341 deletions(-) create mode 100644 game/plugins/__init__.py create mode 100644 game/plugins/luaplugin.py create mode 100644 game/plugins/manager.py delete mode 100644 plugin/__init__.py delete mode 100644 plugin/luaplugin.py delete mode 100644 plugin/manager.py create mode 100644 qt_ui/windows/settings/plugins.py diff --git a/game/game.py b/game/game.py index 0d7b1f28..775f36f3 100644 --- a/game/game.py +++ b/game/game.py @@ -3,7 +3,7 @@ import math import random import sys from datetime import date, datetime, timedelta -from typing import Any, Dict, List +from typing import Dict, List from dcs.action import Coalition from dcs.mapping import Point @@ -15,6 +15,7 @@ from game import db from game.db import PLAYER_BUDGET_BASE, REWARDS from game.inventory import GlobalAircraftInventory from game.models.game_stats import GameStats +from game.plugins import LuaPluginManager from gen.ato import AirTaskingOrder from gen.conflictgen import Conflict from gen.flights.ai_flight_planner import CoalitionMissionPlanner @@ -29,7 +30,6 @@ from .event.frontlineattack import FrontlineAttackEvent from .factions.faction import Faction from .infos.information import Information from .settings import Settings -from plugin import LuaPluginManager from .weather import Conditions, TimeOfDay COMMISION_UNIT_VARIETY = 4 @@ -226,11 +226,8 @@ class Game: return event and event.name and event.name == self.player_name def on_load(self) -> None: + LuaPluginManager.load_settings(self.settings) ObjectiveDistanceCache.set_theater(self.theater) - - # set the settings in all plugins - for plugin in LuaPluginManager().getPlugins(): - plugin.setSettings(self.settings) # Save game compatibility. diff --git a/game/operation/operation.py b/game/operation/operation.py index d8c2f761..0ff06ebe 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -14,6 +14,7 @@ from dcs.translation import String from dcs.triggers import TriggerStart from dcs.unittype import UnitType +from game.plugins import LuaPluginManager from gen import Conflict, FlightType, VisualGenerator from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData from gen.airfields import AIRFIELD_DATA @@ -28,7 +29,6 @@ from gen.kneeboard import KneeboardGenerator from gen.radios import RadioFrequency, RadioRegistry from gen.tacan import TacanRegistry from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator -from plugin import LuaPluginManager from theater import ControlPoint from .. import db from ..debriefing import Debriefing @@ -473,9 +473,10 @@ dcsLiberation.TargetPoints = { self.current_mission.triggerrules.triggers.append(trigger) # Inject Plugins Lua Scripts and data - for plugin in LuaPluginManager().getPlugins(): - plugin.injectScripts(self) - plugin.injectConfiguration(self) + for plugin in LuaPluginManager.plugins(): + if plugin.enabled: + plugin.inject_scripts(self) + plugin.inject_configuration(self) self.assign_channels_to_flights(airgen.flights, airsupportgen.air_support) diff --git a/game/plugins/__init__.py b/game/plugins/__init__.py new file mode 100644 index 00000000..2203739d --- /dev/null +++ b/game/plugins/__init__.py @@ -0,0 +1,2 @@ +from .luaplugin import LuaPlugin +from .manager import LuaPluginManager diff --git a/game/plugins/luaplugin.py b/game/plugins/luaplugin.py new file mode 100644 index 00000000..f48bc185 --- /dev/null +++ b/game/plugins/luaplugin.py @@ -0,0 +1,180 @@ +from __future__ import annotations + +import json +import logging +import textwrap +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.operation.operation import Operation + + +class LuaPluginWorkOrder: + + def __init__(self, parent_mnemonic: str, filename: str, mnemonic: str, + disable: bool) -> None: + self.parent_mnemonic = parent_mnemonic + self.filename = filename + self.mnemonic = mnemonic + self.disable = disable + + def work(self, operation: Operation) -> None: + if self.disable: + operation.bypass_plugin_script(self.mnemonic) + else: + operation.inject_plugin_script(self.parent_mnemonic, self.filename, + self.mnemonic) + + +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): + 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) + + def set_enabled(self, enabled: bool) -> None: + self.settings.set_plugin_option(self.identifier, enabled) + + +class LuaPluginOption(PluginSettings): + def __init__(self, identifier: str, name: str, + enabled_by_default: bool) -> None: + super().__init__(identifier, enabled_by_default) + self.name = name + + +@dataclass(frozen=True) +class LuaPluginDefinition: + identifier: str + name: str + present_in_ui: bool + enabled_by_default: bool + options: List[LuaPluginOption] + work_orders: List[LuaPluginWorkOrder] + config_work_orders: List[LuaPluginWorkOrder] + + @classmethod + def from_json(cls, name: str, path: Path) -> LuaPluginDefinition: + data = json.loads(path.read_text()) + + options = [] + for option in data.get("specificOptions"): + option_id = option["mnemonic"] + options.append(LuaPluginOption( + identifier=f"{name}.{option_id}", + name=option.get("nameInUI", name), + enabled_by_default=option.get("defaultValue") + )) + + work_orders = [] + for work_order in data.get("scriptsWorkOrders"): + work_orders.append(LuaPluginWorkOrder( + name, work_order.get("file"), work_order["mnemonic"], + work_order.get("disable", False) + )) + config_work_orders = [] + for work_order in data.get("configurationWorkOrders"): + config_work_orders.append(LuaPluginWorkOrder( + name, work_order.get("file"), work_order["mnemonic"], + work_order.get("disable", False) + )) + + return cls( + identifier=name, + name=data["nameInUI"], + present_in_ui=not data.get("skipUI", False), + enabled_by_default=data.get("defaultValue", False), + options=options, + work_orders=work_orders, + config_work_orders=config_work_orders + ) + + +class LuaPlugin(PluginSettings): + + def __init__(self, definition: LuaPluginDefinition) -> None: + self.definition = definition + super().__init__(self.definition.identifier, + self.definition.enabled_by_default) + + @property + def name(self) -> str: + return self.definition.name + + @property + def show_in_ui(self) -> bool: + return self.definition.present_in_ui + + @property + def options(self) -> List[LuaPluginOption]: + return self.definition.options + + @classmethod + def from_json(cls, name: str, path: Path) -> Optional[LuaPlugin]: + try: + definition = LuaPluginDefinition.from_json(name, path) + except KeyError: + logging.exception("Required plugin configuration value missing") + return None + + return cls(definition) + + def set_settings(self, settings: Settings): + super().set_settings(settings) + for option in self.definition.options: + option.set_settings(self.settings) + + def inject_scripts(self, operation: Operation) -> None: + for work_order in self.definition.work_orders: + work_order.work(operation) + + def inject_configuration(self, operation: Operation) -> None: + # inject the plugin options + if self.options: + option_decls = [] + for option in self.options: + enabled = str(option.enabled).lower() + name = option.identifier + option_decls.append( + f" dcsLiberation.plugins.{name} = {enabled}") + + joined_options = "\n".join(option_decls) + + lua = textwrap.dedent(f"""\ + -- {self.identifier} plugin configuration. + + if dcsLiberation then + if not dcsLiberation.plugins then + dcsLiberation.plugins = {{}} + end + dcsLiberation.plugins.{self.identifier} = {{}} + {joined_options} + end + + """) + + operation.inject_lua_trigger( + lua, f"{self.identifier} plugin configuration") + + for work_order in self.definition.config_work_orders: + work_order.work(operation) diff --git a/game/plugins/manager.py b/game/plugins/manager.py new file mode 100644 index 00000000..19b87118 --- /dev/null +++ b/game/plugins/manager.py @@ -0,0 +1,50 @@ +import json +import logging +from pathlib import Path +from typing import Dict, List, Optional + +from game.settings import Settings +from game.plugins.luaplugin import LuaPlugin + + +class LuaPluginManager: + _plugins_loaded = False + _plugins: Dict[str, LuaPlugin] = {} + + @classmethod + def _load_plugins(cls) -> None: + plugins_path = Path("resources/plugins") + + path = plugins_path / "plugins.json" + if not path.exists(): + raise RuntimeError(f"{path} does not exist. Cannot continue.") + + logging.info(f"Reading plugins list from {path}") + + data = json.loads(path.read_text()) + for name in data: + plugin_path = plugins_path / name / "plugin.json" + if not plugin_path.exists(): + raise RuntimeError( + f"Invalid plugin configuration: required plugin {name} " + f"does not exist at {plugin_path}") + 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 + + @classmethod + def _get_plugins(cls) -> Dict[str, LuaPlugin]: + if not cls._plugins_loaded: + cls._load_plugins() + return cls._plugins + + @classmethod + def plugins(cls) -> List[LuaPlugin]: + return list(cls._get_plugins().values()) + + @classmethod + def load_settings(cls, settings: Settings) -> None: + for plugin in cls.plugins(): + plugin.set_settings(settings) diff --git a/game/settings.py b/game/settings.py index 97626940..764e5ff5 100644 --- a/game/settings.py +++ b/game/settings.py @@ -1,4 +1,5 @@ -from plugin import LuaPluginManager +from typing import Dict + class Settings: @@ -40,15 +41,30 @@ class Settings: self.perf_culling_distance = 100 # LUA Plugins system - self.plugins = {} - for plugin in LuaPluginManager().getPlugins(): - plugin.setSettings(self) + self.plugins: Dict[str, bool] = {} # Cheating self.show_red_ato = False self.never_delay_player_flights = False + @staticmethod + def plugin_settings_key(identifier: str) -> str: + return f"plugins.{identifier}" + + def initialize_plugin_option(self, identifier: str, + default_value: bool) -> None: + try: + self.plugin_option(identifier) + except KeyError: + self.set_plugin_option(identifier, default_value) + + def plugin_option(self, identifier: str) -> bool: + return self.plugins[self.plugin_settings_key(identifier)] + + def set_plugin_option(self, identifier: str, enabled: bool) -> None: + self.plugins[self.plugin_settings_key(identifier)] = enabled + def __setstate__(self, state) -> None: # __setstate__ is called with the dict of the object being unpickled. We # can provide save compatibility for new settings options (which diff --git a/gen/armor.py b/gen/armor.py index a717b677..5685a120 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -34,7 +34,7 @@ from gen.ground_forces.ai_ground_planner import ( from .callsigns import callsign_for_support_unit from .conflictgen import Conflict from .ground_forces.combat_stance import CombatStance -from plugin import LuaPluginManager +from game.plugins import LuaPluginManager SPREAD_DISTANCE_FACTOR = 0.1, 0.3 SPREAD_DISTANCE_SIZE_FACTOR = 0.1 @@ -140,9 +140,7 @@ class GroundConflictGenerator: self.plan_action_for_groups(self.enemy_stance, enemy_groups, player_groups, self.conflict.heading - 90, self.conflict.to_cp, self.conflict.from_cp) # Add JTAC - jtacPlugin = LuaPluginManager().getPlugin("jtacautolase") - useJTAC = jtacPlugin and jtacPlugin.isEnabled() - if self.game.player_faction.has_jtac and useJTAC: + if self.game.player_faction.has_jtac: n = "JTAC" + str(self.conflict.from_cp.id) + str(self.conflict.to_cp.id) code = 1688 - len(self.jtacs) diff --git a/plugin/__init__.py b/plugin/__init__.py deleted file mode 100644 index 37f3c6d4..00000000 --- a/plugin/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .luaplugin import LuaPlugin -from .manager import LuaPluginManager \ No newline at end of file diff --git a/plugin/luaplugin.py b/plugin/luaplugin.py deleted file mode 100644 index 25d53dc4..00000000 --- a/plugin/luaplugin.py +++ /dev/null @@ -1,208 +0,0 @@ -import json -from pathlib import Path -from typing import List, Optional - -from PySide2.QtCore import Qt -from PySide2.QtWidgets import QCheckBox, QGridLayout, QGroupBox, QLabel - - -class LuaPluginWorkOrder: - - def __init__(self, parent, filename: str, mnemonic: str, - disable: bool) -> None: - self.filename = filename - self.mnemonic = mnemonic - self.disable = disable - self.parent = parent - - def work(self, operation): - if self.disable: - operation.bypass_plugin_script(self.mnemonic) - else: - operation.inject_plugin_script(self.parent.mnemonic, self.filename, - self.mnemonic) - -class LuaPluginSpecificOption: - - def __init__(self, parent, mnemonic: str, nameInUI: str, - defaultValue: bool) -> None: - self.mnemonic = mnemonic - self.nameInUI = nameInUI - self.defaultValue = defaultValue - self.parent = parent - -class LuaPlugin: - NAME_IN_SETTINGS_BASE:str = "plugins." - - def __init__(self, jsonFilename: str) -> None: - self.mnemonic: Optional[str] = None - self.skipUI: bool = False - self.nameInUI: Optional[str] = None - self.nameInSettings: Optional[str] = None - self.defaultValue: bool = False - self.specificOptions: List[LuaPluginSpecificOption] = [] - self.scriptsWorkOrders: List[LuaPluginWorkOrder] = [] - self.configurationWorkOrders: List[LuaPluginWorkOrder] = [] - self.initFromJson(jsonFilename) - self.enabled = self.defaultValue - self.settings = None - - def initFromJson(self, jsonFilename:str): - jsonFile:Path = Path(jsonFilename) - if jsonFile.exists(): - jsonData = json.loads(jsonFile.read_text()) - self.mnemonic = jsonData.get("mnemonic") - self.skipUI = jsonData.get("skipUI", False) - self.nameInUI = jsonData.get("nameInUI") - assert self.mnemonic is not None - self.nameInSettings = LuaPlugin.NAME_IN_SETTINGS_BASE + self.mnemonic - self.defaultValue = jsonData.get("defaultValue", False) - self.specificOptions = [] - for jsonSpecificOption in jsonData.get("specificOptions"): - mnemonic = jsonSpecificOption.get("mnemonic") - nameInUI = jsonSpecificOption.get("nameInUI", mnemonic) - defaultValue = jsonSpecificOption.get("defaultValue") - self.specificOptions.append(LuaPluginSpecificOption(self, mnemonic, nameInUI, defaultValue)) - self.scriptsWorkOrders = [] - for jsonWorkOrder in jsonData.get("scriptsWorkOrders"): - file = jsonWorkOrder.get("file") - mnemonic = jsonWorkOrder.get("mnemonic") - disable = jsonWorkOrder.get("disable", False) - self.scriptsWorkOrders.append(LuaPluginWorkOrder(self, file, mnemonic, disable)) - self.configurationWorkOrders = [] - for jsonWorkOrder in jsonData.get("configurationWorkOrders"): - file = jsonWorkOrder.get("file") - mnemonic = jsonWorkOrder.get("mnemonic") - disable = jsonWorkOrder.get("disable", False) - self.configurationWorkOrders.append(LuaPluginWorkOrder(self, file, mnemonic, disable)) - - def setupUI(self, settingsWindow, row:int): - # set the game settings - self.setSettings(settingsWindow.game.settings) - - if not self.skipUI: - assert self.nameInSettings is not None - assert self.settings is not None - - # create the plugin choice checkbox interface - self.uiWidget: QCheckBox = QCheckBox() - self.uiWidget.setChecked(self.isEnabled()) - self.uiWidget.toggled.connect(lambda: self.applySetting(settingsWindow)) - - settingsWindow.pluginsGroupLayout.addWidget(QLabel(self.nameInUI), row, 0) - settingsWindow.pluginsGroupLayout.addWidget(self.uiWidget, row, 1, Qt.AlignRight) - - # if needed, create the plugin options special page - if settingsWindow.pluginsOptionsPageLayout and self.specificOptions != None: - self.optionsGroup: QGroupBox = QGroupBox(self.nameInUI) - optionsGroupLayout = QGridLayout(); - optionsGroupLayout.setAlignment(Qt.AlignTop) - self.optionsGroup.setLayout(optionsGroupLayout) - settingsWindow.pluginsOptionsPageLayout.addWidget(self.optionsGroup) - - # browse each option in the specific options list - row = 0 - for specificOption in self.specificOptions: - assert specificOption.mnemonic is not None - nameInSettings = self.nameInSettings + "." + specificOption.mnemonic - if not nameInSettings in self.settings.plugins: - self.settings.plugins[nameInSettings] = specificOption.defaultValue - - specificOption.uiWidget = QCheckBox() - specificOption.uiWidget.setChecked(self.settings.plugins[nameInSettings]) - #specificOption.uiWidget.setEnabled(False) - specificOption.uiWidget.toggled.connect(lambda: self.applySetting(settingsWindow)) - - optionsGroupLayout.addWidget(QLabel(specificOption.nameInUI), row, 0) - optionsGroupLayout.addWidget(specificOption.uiWidget, row, 1, Qt.AlignRight) - - row += 1 - - # disable or enable the UI in the plugins special page - self.enableOptionsGroup() - - def enableOptionsGroup(self): - if self.optionsGroup: - self.optionsGroup.setEnabled(self.isEnabled()) - - def setSettings(self, settings): - self.settings = settings - - # ensure the setting exist - if not self.nameInSettings in self.settings.plugins: - self.settings.plugins[self.nameInSettings] = self.defaultValue - - # do the same for each option in the specific options list - for specificOption in self.specificOptions: - nameInSettings = self.nameInSettings + "." + specificOption.mnemonic - if not nameInSettings in self.settings.plugins: - self.settings.plugins[nameInSettings] = specificOption.defaultValue - - def applySetting(self, settingsWindow): - # apply the main setting - self.settings.plugins[self.nameInSettings] = self.uiWidget.isChecked() - self.enabled = self.settings.plugins[self.nameInSettings] - - # do the same for each option in the specific options list - for specificOption in self.specificOptions: - nameInSettings = self.nameInSettings + "." + specificOption.mnemonic - self.settings.plugins[nameInSettings] = specificOption.uiWidget.isChecked() - - # disable or enable the UI in the plugins special page - self.enableOptionsGroup() - - def injectScripts(self, operation): - # set the game settings - self.setSettings(operation.game.settings) - - # execute the work order - if self.scriptsWorkOrders != None: - for workOrder in self.scriptsWorkOrders: - workOrder.work(operation) - - # serves for subclasses - return self.isEnabled() - - def injectConfiguration(self, operation): - # set the game settings - self.setSettings(operation.game.settings) - - # inject the plugin options - if len(self.specificOptions) > 0: - defineAllOptions = "" - for specificOption in self.specificOptions: - nameInSettings = self.nameInSettings + "." + specificOption.mnemonic - value = "true" if self.settings.plugins[nameInSettings] else "false" - defineAllOptions += f" dcsLiberation.plugins.{self.mnemonic}.{specificOption.mnemonic} = {value} \n" - - - lua = f"-- {self.mnemonic} plugin configuration.\n" - lua += "\n" - lua += "if dcsLiberation then\n" - lua += " if not dcsLiberation.plugins then \n" - lua += " dcsLiberation.plugins = {}\n" - lua += " end\n" - lua += f" dcsLiberation.plugins.{self.mnemonic} = {{}}\n" - lua += defineAllOptions - lua += "end" - - operation.inject_lua_trigger(lua, f"{self.mnemonic} plugin configuration") - - # execute the work order - if self.configurationWorkOrders != None: - for workOrder in self.configurationWorkOrders: - workOrder.work(operation) - - # serves for subclasses - return self.isEnabled() - - def isEnabled(self) -> bool: - if not self.settings: - return False - - self.setSettings(self.settings) # create the necessary settings keys if needed - - return self.settings != None and self.settings.plugins[self.nameInSettings] - - def hasUI(self) -> bool: - return not self.skipUI \ No newline at end of file diff --git a/plugin/manager.py b/plugin/manager.py deleted file mode 100644 index d7625821..00000000 --- a/plugin/manager.py +++ /dev/null @@ -1,43 +0,0 @@ -from .luaplugin import LuaPlugin -from typing import List -import glob -from pathlib import Path -import json -import logging - - -class LuaPluginManager(): - PLUGINS_RESOURCE_PATH = Path("resources/plugins") - PLUGINS_LIST_FILENAME = "plugins.json" - PLUGINS_JSON_FILENAME = "plugin.json" - - __plugins = None - def __init__(self): - if not LuaPluginManager.__plugins: - LuaPluginManager.__plugins= [] - jsonFile:Path = Path(LuaPluginManager.PLUGINS_RESOURCE_PATH, LuaPluginManager.PLUGINS_LIST_FILENAME) - if jsonFile.exists(): - logging.info(f"Reading plugins list from {jsonFile}") - - jsonData = json.loads(jsonFile.read_text()) - for plugin in jsonData: - jsonPluginFolder = Path(LuaPluginManager.PLUGINS_RESOURCE_PATH, plugin) - jsonPluginFile = Path(jsonPluginFolder, LuaPluginManager.PLUGINS_JSON_FILENAME) - if jsonPluginFile.exists(): - logging.info(f"Reading plugin {plugin} from {jsonPluginFile}") - plugin = LuaPlugin(jsonPluginFile) - LuaPluginManager.__plugins.append(plugin) - else: - logging.error(f"Missing configuration file {jsonPluginFile} for plugin {plugin}") - else: - logging.error(f"Missing plugins list file {jsonFile}") - - def getPlugins(self): - return LuaPluginManager.__plugins - - def getPlugin(self, pluginName): - for plugin in LuaPluginManager.__plugins: - if plugin.mnemonic == pluginName: - return plugin - - return None \ No newline at end of file diff --git a/qt_ui/windows/settings/QSettingsWindow.py b/qt_ui/windows/settings/QSettingsWindow.py index 3597a179..486068f5 100644 --- a/qt_ui/windows/settings/QSettingsWindow.py +++ b/qt_ui/windows/settings/QSettingsWindow.py @@ -26,7 +26,8 @@ from game.infos.information import Information from qt_ui.widgets.QLabeledWidget import QLabeledWidget from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.finances.QFinancesMenu import QHorizontalSeparationLine -from plugin import LuaPluginManager +from qt_ui.windows.settings.plugins import PluginOptionsPage, PluginsPage + class CheatSettingsBox(QGroupBox): def __init__(self, game: Game, apply_settings: Callable[[], None]) -> None: @@ -97,21 +98,21 @@ class QSettingsWindow(QDialog): self.categoryModel.appendRow(cheat) self.right_layout.addWidget(self.cheatPage) - self.initPluginsLayout() - if self.pluginsPage: - plugins = QStandardItem("LUA Plugins") - plugins.setIcon(CONST.ICONS["Plugins"]) - plugins.setEditable(False) - plugins.setSelectable(True) - self.categoryModel.appendRow(plugins) - self.right_layout.addWidget(self.pluginsPage) - if self.pluginsOptionsPage: - pluginsOptions = QStandardItem("LUA Plugins Options") - pluginsOptions.setIcon(CONST.ICONS["PluginsOptions"]) - pluginsOptions.setEditable(False) - pluginsOptions.setSelectable(True) - self.categoryModel.appendRow(pluginsOptions) - self.right_layout.addWidget(self.pluginsOptionsPage) + self.pluginsPage = PluginsPage() + plugins = QStandardItem("LUA Plugins") + plugins.setIcon(CONST.ICONS["Plugins"]) + plugins.setEditable(False) + plugins.setSelectable(True) + self.categoryModel.appendRow(plugins) + self.right_layout.addWidget(self.pluginsPage) + + self.pluginsOptionsPage = PluginOptionsPage() + pluginsOptions = QStandardItem("LUA Plugins Options") + pluginsOptions.setIcon(CONST.ICONS["PluginsOptions"]) + pluginsOptions.setEditable(False) + pluginsOptions.setSelectable(True) + self.categoryModel.appendRow(pluginsOptions) + self.right_layout.addWidget(self.pluginsOptionsPage) self.categoryList.setSelectionBehavior(QAbstractItemView.SelectRows) self.categoryList.setModel(self.categoryModel) @@ -330,34 +331,6 @@ class QSettingsWindow(QDialog): self.moneyCheatBoxLayout.addWidget(btn, i/2, i%2) self.cheatLayout.addWidget(self.moneyCheatBox, stretch=1) - def initPluginsLayout(self): - uiPrepared = False - row:int = 0 - for plugin in LuaPluginManager().getPlugins(): - if plugin.hasUI(): - if not uiPrepared: - uiPrepared = True - - self.pluginsOptionsPage = QWidget() - self.pluginsOptionsPageLayout = QVBoxLayout() - self.pluginsOptionsPageLayout.setAlignment(Qt.AlignTop) - self.pluginsOptionsPage.setLayout(self.pluginsOptionsPageLayout) - - self.pluginsPage = QWidget() - self.pluginsPageLayout = QVBoxLayout() - self.pluginsPageLayout.setAlignment(Qt.AlignTop) - self.pluginsPage.setLayout(self.pluginsPageLayout) - - self.pluginsGroup = QGroupBox("Plugins") - self.pluginsGroupLayout = QGridLayout(); - self.pluginsGroupLayout.setAlignment(Qt.AlignTop) - self.pluginsGroup.setLayout(self.pluginsGroupLayout) - - self.pluginsPageLayout.addWidget(self.pluginsGroup) - - plugin.setupUI(self, row) - row = row + 1 - def cheatLambda(self, amount): return lambda: self.cheatMoney(amount) diff --git a/qt_ui/windows/settings/plugins.py b/qt_ui/windows/settings/plugins.py new file mode 100644 index 00000000..ca3f6e35 --- /dev/null +++ b/qt_ui/windows/settings/plugins.py @@ -0,0 +1,71 @@ +from PySide2.QtCore import Qt +from PySide2.QtWidgets import ( + QCheckBox, + QGridLayout, + QGroupBox, + QLabel, QVBoxLayout, + QWidget, +) + +from game.plugins import LuaPlugin, LuaPluginManager + + +class PluginsBox(QGroupBox): + def __init__(self) -> None: + super().__init__("Plugins") + + layout = QGridLayout() + layout.setAlignment(Qt.AlignTop) + self.setLayout(layout) + + for row, plugin in enumerate(LuaPluginManager.plugins()): + if not plugin.show_in_ui: + continue + + layout.addWidget(QLabel(plugin.name), row, 0) + + checkbox = QCheckBox() + checkbox.setChecked(plugin.enabled) + checkbox.toggled.connect(plugin.set_enabled) + layout.addWidget(checkbox, row, 1) + + +class PluginsPage(QWidget): + def __init__(self) -> None: + super().__init__() + + layout = QVBoxLayout() + layout.setAlignment(Qt.AlignTop) + self.setLayout(layout) + + layout.addWidget(PluginsBox()) + + +class PluginOptionsBox(QGroupBox): + def __init__(self, plugin: LuaPlugin) -> None: + super().__init__(plugin.name) + + layout = QGridLayout() + layout.setAlignment(Qt.AlignTop) + self.setLayout(layout) + + for row, option in enumerate(plugin.options): + layout.addWidget(QLabel(option.name), row, 0) + + checkbox = QCheckBox() + checkbox.setChecked(option.enabled) + checkbox.toggled.connect(option.set_enabled) + layout.addWidget(checkbox, row, 1) + + +class PluginOptionsPage(QWidget): + def __init__(self) -> None: + super().__init__() + + layout = QVBoxLayout() + layout.setAlignment(Qt.AlignTop) + self.setLayout(layout) + + for plugin in LuaPluginManager.plugins(): + if plugin.options: + layout.addWidget(PluginOptionsBox(plugin)) diff --git a/resources/plugins/_doc/plugins_readme.md b/resources/plugins/_doc/plugins_readme.md index 7df75d91..8af9802b 100644 --- a/resources/plugins/_doc/plugins_readme.md +++ b/resources/plugins/_doc/plugins_readme.md @@ -15,8 +15,7 @@ This file is the description of the plugin. The *base* and *jtacautolase* plugins both are included in the standard dcs-liberation distribution. You can check their respective `plugin.json` files to understand how they work. Here's a quick rundown of the file's components : - -- `mnemonic` : the short, technical name of the plugin. It's the name of the folder, and the name of the plugin in the application's settings + - `skipUI` : if *true*, this plugin will not appear in the plugins selection user interface. Useful to force a plugin ON or OFF (see the *base* plugin) - `nameInUI` : the title of the plugin as it will appear in the plugins selection user interface. - `defaultValue` : the selection value of the plugin, when first installed ; if true, plugin is selected. @@ -41,29 +40,14 @@ It is mandatory. This plugin replaces the vanilla JTAC functionality in dcs-liberation. -### The *VEAF framework* plugin +### Known third-party plugins -When enabled, this plugin will inject and configure the VEAF Framework scripts in the mission. +Plugins not included with Liberation can be installed by adding them to the +`resources/plugins` directory and listing them in +`resources/plugins/plugins.json`. Below is a list of other plugins that can be +installed: -These scripts add a lot of runtime functionalities : - -- spawning of units and groups (and portable TACANs) -- air-to-ground missions -- air-to-air missions -- transport missions -- carrier operations (not Moose) -- tanker move -- weather and ATC -- shelling a zone, lighting it up -- managing assets (tankers, awacs, aircraft carriers) : getting info, state, respawning them if needed -- managing named points (position, info, ATC) -- managing a dynamic radio menu -- managing remote calls to the mission through NIOD (RPC) and SLMOD (LUA sockets) -- managing security (not allowing everyone to do every action) -- define groups templates - -You can find the *VEAF Framework* plugin [on GitHub](https://github.com/VEAF/dcs-liberation-veaf-framework/releases) -For more information, please visit the [VEAF Framework documentation site](https://veaf.github.io/VEAF-Mission-Creation-Tools/) (work in progress) +* [VEAF](https://github.com/VEAF/dcs-liberation-veaf-framework) ## Custom plugins diff --git a/resources/plugins/base/plugin.json b/resources/plugins/base/plugin.json index 2234980e..97863e88 100644 --- a/resources/plugins/base/plugin.json +++ b/resources/plugins/base/plugin.json @@ -1,5 +1,4 @@ { - "mnemonic": "base", "skipUI": true, "nameInUI": "", "defaultValue": true, diff --git a/resources/plugins/jtacautolase/plugin.json b/resources/plugins/jtacautolase/plugin.json index 33ce09dd..18e7ca5a 100644 --- a/resources/plugins/jtacautolase/plugin.json +++ b/resources/plugins/jtacautolase/plugin.json @@ -1,5 +1,4 @@ { - "mnemonic": "jtacautolase", "nameInUI": "JTAC Autolase", "defaultValue": true, "specificOptions": [ diff --git a/resources/plugins/skynetiads/plugin.json b/resources/plugins/skynetiads/plugin.json index c56298d9..78f2beab 100644 --- a/resources/plugins/skynetiads/plugin.json +++ b/resources/plugins/skynetiads/plugin.json @@ -1,5 +1,4 @@ { - "mnemonic": "skynetiads", "nameInUI": "Skynet IADS", "defaultValue": false, "specificOptions": [