diff --git a/.gitignore b/.gitignore index 1bf595f6..f058e01f 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,5 @@ logs/ qt_ui/logs/liberation.log *.psd -resources/scripts/plugins/* +plugin/custom/__plugins.lst +plugin/custom/*.lua diff --git a/.gitmodules b/.gitmodules index d8db9cf5..e4041d5d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,6 @@ path = pydcs url = https://github.com/pydcs/dcs branch = master +[submodule "plugin/veaf"] + path = plugin/veaf + url = https://github.com/VEAF/dcs-liberation-veaf-framework diff --git a/game/operation/operation.py b/game/operation/operation.py index 66eddb78..b537395e 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -31,7 +31,7 @@ from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator from theater import ControlPoint from .. import db from ..debriefing import Debriefing - +from plugin import BasePlugin, INSTALLED_PLUGINS class Operation: attackers_starting_position = None # type: db.StartingPosition @@ -75,6 +75,7 @@ class Operation: self.departure_cp = departure_cp self.to_cp = to_cp self.is_quick = False + self.listOfPluginsScripts = [] def units_of(self, country_name: str) -> List[UnitType]: return [] @@ -133,6 +134,36 @@ class Operation: else: self.defenders_starting_position = None + def injectLuaTrigger(self, luascript, comment = "LUA script"): + trigger = TriggerStart(comment=comment) + trigger.add_action(DoScript(String(luascript))) + self.current_mission.triggerrules.triggers.append(trigger) + + def bypassPluginScript(self, pluginName, scriptFileMnemonic): + self.listOfPluginsScripts.append(scriptFileMnemonic) + + def injectPluginScript(self, pluginName, scriptFile, scriptFileMnemonic): + if not scriptFileMnemonic in self.listOfPluginsScripts: + self.listOfPluginsScripts.append(scriptFileMnemonic) + + if pluginName == None: + pluginName = "custom" + plugin_path = Path("./plugin",pluginName) + + if scriptFile != None: + scriptFile_path = Path(plugin_path, scriptFile) + if scriptFile_path.exists(): + trigger = TriggerStart(comment="Load " + scriptFileMnemonic) + filename = scriptFile_path.resolve() + fileref = self.current_mission.map_resource.add_resource_file(filename) + trigger.add_action(DoScriptFile(fileref)) + self.current_mission.triggerrules.triggers.append(trigger) + else: + logging.error(f"Cannot find script file {scriptFile} for plugin {pluginName}") + + else: + logging.debug(f"Skipping script file {scriptFile} for plugin {pluginName}") + def generate(self): radio_registry = RadioRegistry() tacan_registry = TacanRegistry() @@ -434,67 +465,12 @@ dcsLiberation.TargetPoints = { trigger.add_action(DoScript(String(lua))) self.current_mission.triggerrules.triggers.append(trigger) - # Inject Plugins Lua Scripts - listOfPluginsScripts = [] - plugin_file_path = Path("./resources/scripts/plugins/__plugins.lst") - if plugin_file_path.exists(): - for line in plugin_file_path.read_text().splitlines(): - name = line.strip() - if not name.startswith( '#' ): - trigger = TriggerStart(comment="Load " + name) - listOfPluginsScripts.append(name) - fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/plugins/" + name) - trigger.add_action(DoScriptFile(fileref)) - self.current_mission.triggerrules.triggers.append(trigger) - else: - logging.info( - f"Not loading plugins, {plugin_file_path} does not exist") + # Inject Plugins Lua Scripts and data + self.listOfPluginsScripts = [] - # Inject Mist Script if not done already in the plugins - if not "mist.lua" in listOfPluginsScripts and not "mist_4_3_74.lua" in listOfPluginsScripts: # don't load the script twice - trigger = TriggerStart(comment="Load Mist Lua framework") - fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/mist_4_3_74.lua") - trigger.add_action(DoScriptFile(fileref)) - self.current_mission.triggerrules.triggers.append(trigger) - - # Inject JSON library if not done already in the plugins - if not "json.lua" in listOfPluginsScripts : # don't load the script twice - trigger = TriggerStart(comment="Load JSON Lua library") - fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/json.lua") - trigger.add_action(DoScriptFile(fileref)) - self.current_mission.triggerrules.triggers.append(trigger) - - # Inject Ciribob's JTACAutoLase if not done already in the plugins - if not "JTACAutoLase.lua" in listOfPluginsScripts : # don't load the script twice - trigger = TriggerStart(comment="Load JTACAutoLase.lua script") - fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/JTACAutoLase.lua") - trigger.add_action(DoScriptFile(fileref)) - self.current_mission.triggerrules.triggers.append(trigger) - - # Inject DCS-Liberation script if not done already in the plugins - if not "dcs_liberation.lua" in listOfPluginsScripts : # don't load the script twice - trigger = TriggerStart(comment="Load DCS Liberation script") - fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/dcs_liberation.lua") - trigger.add_action(DoScriptFile(fileref)) - self.current_mission.triggerrules.triggers.append(trigger) - - # add a configuration for JTACAutoLase and start lasing for all JTACs - smoke = "true" - if hasattr(self.game.settings, "jtac_smoke_on"): - if not self.game.settings.jtac_smoke_on: - smoke = "false" - - lua = """ - -- setting and starting JTACs - env.info("DCSLiberation|: setting and starting JTACs") - """ - - for jtac in jtacs: - lua += f"if dcsLiberation.JTACAutoLase then dcsLiberation.JTACAutoLase('{jtac.unit_name}', {jtac.code}, {smoke}, 'vehicle') end\n" - - trigger = TriggerStart(comment="Start JTACs") - trigger.add_action(DoScript(String(lua))) - self.current_mission.triggerrules.triggers.append(trigger) + for plugin in INSTALLED_PLUGINS: + plugin.injectScripts(self) + plugin.injectConfiguration(self) self.assign_channels_to_flights(airgen.flights, airsupportgen.air_support) diff --git a/game/settings.py b/game/settings.py index 4566ad0f..5d0d5c91 100644 --- a/game/settings.py +++ b/game/settings.py @@ -40,4 +40,7 @@ class Settings: self.perf_culling = False self.perf_culling_distance = 100 + # LUA Plugins system + self.plugins = {} + diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 561f359c..03fe6d32 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -205,7 +205,7 @@ class PackageBuilder: airfield, aircraft = assignment flight = Flight(aircraft, plan.num_aircraft, airfield, plan.task) self.package.add_flight(flight) - flight.targetPoint = location + flight.targetPoint = self.package.target return True def build(self) -> Package: @@ -218,7 +218,7 @@ class PackageBuilder: for flight in flights: self.global_inventory.return_from_flight(flight) self.package.remove_flight(flight) - flight.targetPoint = None + flight.targetPoint = None class ObjectiveFinder: """Identifies potential objectives for the mission planner.""" diff --git a/plugin/__init__.py b/plugin/__init__.py new file mode 100644 index 00000000..c3cd6fb2 --- /dev/null +++ b/plugin/__init__.py @@ -0,0 +1,10 @@ +from .base_plugin import BasePlugin +from .veaf_plugin import VeafPlugin +from .jtacautolase_plugin import JtacAutolasePlugin +from .liberation_plugin import LiberationPlugin + +INSTALLED_PLUGINS=[ + VeafPlugin(), + JtacAutolasePlugin(), + LiberationPlugin() + ] \ No newline at end of file diff --git a/resources/scripts/dcs_liberation.lua b/plugin/base/dcs_liberation.lua similarity index 100% rename from resources/scripts/dcs_liberation.lua rename to plugin/base/dcs_liberation.lua diff --git a/resources/scripts/json.lua b/plugin/base/json.lua similarity index 100% rename from resources/scripts/json.lua rename to plugin/base/json.lua diff --git a/resources/scripts/mist_4_3_74.lua b/plugin/base/mist_4_3_74.lua similarity index 100% rename from resources/scripts/mist_4_3_74.lua rename to plugin/base/mist_4_3_74.lua diff --git a/plugin/base_plugin.py b/plugin/base_plugin.py new file mode 100644 index 00000000..c2a850e2 --- /dev/null +++ b/plugin/base_plugin.py @@ -0,0 +1,41 @@ +from PySide2.QtCore import QSize, Qt, QItemSelectionModel, QPoint +from PySide2.QtWidgets import QLabel, QDialog, QGridLayout, QListView, QStackedLayout, QComboBox, QWidget, \ + QAbstractItemView, QPushButton, QGroupBox, QCheckBox, QVBoxLayout, QSpinBox + +class BasePlugin(): + nameInUI:str = "Base plugin" + nameInSettings:str = "plugin.base" + enabledDefaultValue:bool = False + + def __init__(self): + self.uiWidget: QCheckBox = None + self.enabled = self.enabledDefaultValue + self.settings = None + + def setupUI(self, settingsWindow, row:int): + self.settings = settingsWindow.game.settings + + if not self.nameInSettings in self.settings.plugins: + self.settings.plugins[self.nameInSettings] = self.enabledDefaultValue + + self.uiWidget = QCheckBox() + self.uiWidget.setChecked(self.settings.plugins[self.nameInSettings]) + 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) + + def applySetting(self, settingsWindow): + self.settings.plugins[self.nameInSettings] = self.uiWidget.isChecked() + self.enabled = self.settings.plugins[self.nameInSettings] + + def injectScripts(self, operation): + self.settings = operation.game.settings + return self.isEnabled() + + def injectConfiguration(self, operation): + self.settings = operation.game.settings + return self.isEnabled() + + def isEnabled(self) -> bool: + return self.settings != None and self.settings.plugins[self.nameInSettings] diff --git a/resources/scripts/plugins/__plugins.lst.sample b/plugin/custom/__plugins.lst.sample similarity index 100% rename from resources/scripts/plugins/__plugins.lst.sample rename to plugin/custom/__plugins.lst.sample diff --git a/resources/scripts/JTACAutoLase.lua b/plugin/jtacautolase/JTACAutoLase.lua similarity index 100% rename from resources/scripts/JTACAutoLase.lua rename to plugin/jtacautolase/JTACAutoLase.lua diff --git a/plugin/jtacautolase_plugin.py b/plugin/jtacautolase_plugin.py new file mode 100644 index 00000000..d8becf03 --- /dev/null +++ b/plugin/jtacautolase_plugin.py @@ -0,0 +1,80 @@ +from dcs.triggers import TriggerStart +from PySide2.QtCore import QSize, Qt, QItemSelectionModel, QPoint +from PySide2.QtWidgets import QLabel, QDialog, QGridLayout, QListView, QStackedLayout, QComboBox, QWidget, \ + QAbstractItemView, QPushButton, QGroupBox, QCheckBox, QVBoxLayout, QSpinBox +from .base_plugin import BasePlugin + +class JtacAutolasePlugin(BasePlugin): + nameInUI:str = "JTAC Autolase" + nameInSettings:str = "plugin.jtacAutolase" + enabledDefaultValue:bool = True + + #Allow spawn option + nameInUI_useSmoke:str = "JTACs use smoke" + nameInSettings_useSmoke:str = "plugin.jtacAutolase.useSmoke" + + def setupUI(self, settingsWindow, row:int): + # call the base method to add the plugin selection checkbox + super().setupUI(settingsWindow, row) + + if settingsWindow.pluginsOptionsPageLayout: + self.optionsGroup = QGroupBox(self.nameInUI) + optionsGroupLayout = QGridLayout(); + optionsGroupLayout.setAlignment(Qt.AlignTop) + self.optionsGroup.setLayout(optionsGroupLayout) + settingsWindow.pluginsOptionsPageLayout.addWidget(self.optionsGroup) + + # JTAC use smoke + if not self.nameInSettings_useSmoke in self.settings.plugins: + self.settings.plugins[self.nameInSettings_useSmoke] = True + + self.uiWidget_useSmoke = QCheckBox() + self.uiWidget_useSmoke.setChecked(self.settings.plugins[self.nameInSettings_useSmoke]) + self.uiWidget_useSmoke.toggled.connect(lambda: self.applySetting(settingsWindow)) + + optionsGroupLayout.addWidget(QLabel(self.nameInUI_useSmoke), 0, 0) + optionsGroupLayout.addWidget(self.uiWidget_useSmoke, 0, 1, Qt.AlignRight) + + # disable or enable the UI in the plugins special page + self.enableOptionsGroup() + + def enableOptionsGroup(self): + pluginEnabled = self.uiWidget.isChecked() + self.optionsGroup.setEnabled(pluginEnabled) + + def applySetting(self, settingsWindow): + # call the base method to apply the plugin selection checkbox value + super().applySetting(settingsWindow) + + # save the "use smoke" option + self.settings.plugins[self.nameInSettings_useSmoke] = self.uiWidget_useSmoke.isChecked() + + # disable or enable the UI in the plugins special page + self.enableOptionsGroup() + + def injectScripts(self, operation): + if super().injectScripts(operation): + operation.injectPluginScript("jtacautolase", "JTACAutoLase.lua", "jtacautolase") + + def injectConfiguration(self, operation): + if super().injectConfiguration(operation): + + # add a configuration for JTACAutoLase and start lasing for all JTACs + smoke = "local smoke = false" + if self.settings.plugins[self.nameInSettings_useSmoke]: + smoke = "local smoke = true" + + lua = smoke + """ + + -- setting and starting JTACs + env.info("DCSLiberation|: setting and starting JTACs") + + for _, jtac in pairs(dcsLiberation.JTACs) do + if dcsLiberation.JTACAutoLase then + dcsLiberation.JTACAutoLase(jtac.dcsUnit, jtac.code, smoke, 'vehicle') + end + end + """ + + operation.injectLuaTrigger(lua, "Setting and starting JTACs") + diff --git a/plugin/liberation_plugin.py b/plugin/liberation_plugin.py new file mode 100644 index 00000000..e9a01a39 --- /dev/null +++ b/plugin/liberation_plugin.py @@ -0,0 +1,20 @@ +from .base_plugin import BasePlugin + +class LiberationPlugin(BasePlugin): + nameInUI:str = "Liberation script" + nameInSettings:str = "plugin.liberation" + enabledDefaultValue:bool = True + + def setupUI(self, settingsWindow, row:int): + # Don't setup any UI, this plugin is mandatory + pass + + def injectScripts(self, operation): + if super().injectScripts(operation): + operation.injectPluginScript("base", "mist_4_3_74.lua", "mist") + operation.injectPluginScript("base", "json.lua", "json") + operation.injectPluginScript("base", "dcs_liberation.lua", "liberation") + + def injectConfiguration(self, operation): + if super().injectConfiguration(operation): + pass diff --git a/plugin/veaf b/plugin/veaf new file mode 160000 index 00000000..219cdffe --- /dev/null +++ b/plugin/veaf @@ -0,0 +1 @@ +Subproject commit 219cdffef087660fe448a41e1f187c4856e9d80f diff --git a/plugin/veaf_plugin.py b/plugin/veaf_plugin.py new file mode 100644 index 00000000..2e3b0531 --- /dev/null +++ b/plugin/veaf_plugin.py @@ -0,0 +1,90 @@ +from PySide2.QtCore import QSize, Qt, QItemSelectionModel, QPoint +from PySide2.QtWidgets import QLabel, QDialog, QGridLayout, QListView, QStackedLayout, QComboBox, QWidget, \ + QAbstractItemView, QPushButton, QGroupBox, QCheckBox, QVBoxLayout, QSpinBox +from .base_plugin import BasePlugin + +class VeafPlugin(BasePlugin): + nameInUI:str = "VEAF framework" + nameInSettings:str = "plugin.veaf" + enabledDefaultValue:bool = False + + #Allow spawn option + nameInUI_allowSpawn:str = "Allow units spawn via markers and CTLD (not implemented yet)" + nameInSettings_allowSpawn:str = "plugin.veaf.allowSpawn" + + def setupUI(self, settingsWindow, row:int): + # call the base method to add the plugin selection checkbox + super().setupUI(settingsWindow, row) + + if settingsWindow.pluginsOptionsPageLayout: + self.optionsGroup = QGroupBox(self.nameInUI) + optionsGroupLayout = QGridLayout(); + optionsGroupLayout.setAlignment(Qt.AlignTop) + self.optionsGroup.setLayout(optionsGroupLayout) + settingsWindow.pluginsOptionsPageLayout.addWidget(self.optionsGroup) + + # allow spawn of objects + if not self.nameInSettings_allowSpawn in self.settings.plugins: + self.settings.plugins[self.nameInSettings_allowSpawn] = True + + self.uiWidget_allowSpawn = QCheckBox() + self.uiWidget_allowSpawn.setChecked(self.settings.plugins[self.nameInSettings_allowSpawn]) + self.uiWidget_allowSpawn.setEnabled(False) + self.uiWidget_allowSpawn.toggled.connect(lambda: self.applySetting(settingsWindow)) + + optionsGroupLayout.addWidget(QLabel(self.nameInUI_allowSpawn), 0, 0) + optionsGroupLayout.addWidget(self.uiWidget_allowSpawn, 0, 1, Qt.AlignRight) + + # disable or enable the UI in the plugins special page + self.enableOptionsGroup() + + def enableOptionsGroup(self): + pluginEnabled = self.uiWidget.isChecked() + self.optionsGroup.setEnabled(pluginEnabled) + + def applySetting(self, settingsWindow): + # call the base method to apply the plugin selection checkbox value + super().applySetting(settingsWindow) + + # save the "allow spawn" option + self.settings.plugins[self.nameInSettings_allowSpawn] = self.uiWidget_allowSpawn.isChecked() + + # disable or enable the UI in the plugins special page + self.enableOptionsGroup() + + def injectScripts(self, operation): + if super().injectScripts(operation): + # bypass JTACAutoLase + operation.bypassPluginScript("veaf", "jtacautolase") + + # inject the required scripts + operation.injectPluginScript("veaf", "src\\scripts\\mist.lua", "mist") + operation.injectPluginScript("veaf", "src\\scripts\\Moose.lua", "moose") + operation.injectPluginScript("veaf", "src\\scripts\\CTLD.lua", "ctld") + operation.injectPluginScript("veaf", "src\\scripts\\NIOD.lua", "niod") + operation.injectPluginScript("veaf", "src\\scripts\\WeatherMark.lua", "weathermark") + operation.injectPluginScript("veaf", "src\\scripts\\veaf.lua", "veaf") + operation.injectPluginScript("veaf", "src\\scripts\\dcsUnits.lua", "dcsunits") + operation.injectPluginScript("veaf", "src\\scripts\\veafAssets.lua", "veafassets") + operation.injectPluginScript("veaf", "src\\scripts\\veafCarrierOperations.lua", "veafcarrieroperations") + operation.injectPluginScript("veaf", "src\\scripts\\veafCasMission.lua", "veafcasmission") + operation.injectPluginScript("veaf", "src\\scripts\\veafCombatMission.lua", "veafcombatmission") + operation.injectPluginScript("veaf", "src\\scripts\\veafCombatZone.lua", "veafcombatzone") + operation.injectPluginScript("veaf", "src\\scripts\\veafGrass.lua", "veafgrass") + operation.injectPluginScript("veaf", "src\\scripts\\veafInterpreter.lua", "veafinterpreter") + operation.injectPluginScript("veaf", "src\\scripts\\veafMarkers.lua", "veafmarkers") + operation.injectPluginScript("veaf", "src\\scripts\\veafMove.lua", "veafmove") + operation.injectPluginScript("veaf", "src\\scripts\\veafNamedPoints.lua", "veafnamedpoints") + operation.injectPluginScript("veaf", "src\\scripts\\veafRadio.lua", "veafradio") + operation.injectPluginScript("veaf", "src\\scripts\\veafRemote.lua", "veafremote") + operation.injectPluginScript("veaf", "src\\scripts\\veafSecurity.lua", "veafsecurity") + operation.injectPluginScript("veaf", "src\\scripts\\veafShortcuts.lua", "veafshortcuts") + operation.injectPluginScript("veaf", "src\\scripts\\veafSpawn.lua", "veafspawn") + operation.injectPluginScript("veaf", "src\\scripts\\veafTransportMission.lua", "veaftransportmission") + operation.injectPluginScript("veaf", "src\\scripts\\veafUnits.lua", "veafunits") + + + def injectConfiguration(self, operation): + if super().injectConfiguration(operation): + operation.injectPluginScript("veaf", "src\\config\\missionConfig.lua", "missionconfig") + diff --git a/qt_ui/uiconstants.py b/qt_ui/uiconstants.py index 5c97dc72..5c831c1e 100644 --- a/qt_ui/uiconstants.py +++ b/qt_ui/uiconstants.py @@ -99,6 +99,8 @@ def load_icons(): ICONS["Generator"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/generator.png") ICONS["Missile"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/missile.png") ICONS["Cheat"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/cheat.png") + ICONS["Plugins"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/plugins.png") + ICONS["PluginsOptions"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/pluginsoptions.png") ICONS["TaskCAS"] = QPixmap("./resources/ui/tasks/cas.png") ICONS["TaskCAP"] = QPixmap("./resources/ui/tasks/cap.png") diff --git a/qt_ui/windows/settings/QSettingsWindow.py b/qt_ui/windows/settings/QSettingsWindow.py index 6ccdb226..82010be9 100644 --- a/qt_ui/windows/settings/QSettingsWindow.py +++ b/qt_ui/windows/settings/QSettingsWindow.py @@ -11,7 +11,7 @@ from game.game import Game from game.infos.information import Information from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.finances.QFinancesMenu import QHorizontalSeparationLine - +from plugin import BasePlugin, INSTALLED_PLUGINS class QSettingsWindow(QDialog): @@ -52,10 +52,22 @@ class QSettingsWindow(QDialog): cheat.setEditable(False) cheat.setSelectable(True) + plugins = QStandardItem("LUA Plugins") + plugins.setIcon(CONST.ICONS["Plugins"]) + plugins.setEditable(False) + plugins.setSelectable(True) + + pluginsOptions = QStandardItem("LUA Plugins Options") + pluginsOptions.setIcon(CONST.ICONS["PluginsOptions"]) + pluginsOptions.setEditable(False) + pluginsOptions.setSelectable(True) + self.categoryList.setIconSize(QSize(32, 32)) self.categoryModel.appendRow(difficulty) self.categoryModel.appendRow(generator) self.categoryModel.appendRow(cheat) + self.categoryModel.appendRow(plugins) + self.categoryModel.appendRow(pluginsOptions) self.categoryList.setSelectionBehavior(QAbstractItemView.SelectRows) self.categoryList.setModel(self.categoryModel) @@ -65,10 +77,13 @@ class QSettingsWindow(QDialog): self.initDifficultyLayout() self.initGeneratorLayout() self.initCheatLayout() + self.initPluginsLayout() self.right_layout.addWidget(self.difficultyPage) self.right_layout.addWidget(self.generatorPage) self.right_layout.addWidget(self.cheatPage) + self.right_layout.addWidget(self.pluginsPage) + self.right_layout.addWidget(self.pluginsOptionsPage) self.layout.addWidget(self.categoryList, 0, 0, 1, 1) self.layout.addLayout(self.right_layout, 0, 1, 5, 1) @@ -283,6 +298,29 @@ class QSettingsWindow(QDialog): self.moneyCheatBoxLayout.addWidget(btn, i/2, i%2) self.cheatLayout.addWidget(self.moneyCheatBox, 0, 0) + def initPluginsLayout(self): + 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) + + row:int = 0 + for plugin in INSTALLED_PLUGINS: + plugin.setupUI(self, row) + row = row + 1 + + self.pluginsPageLayout.addWidget(self.pluginsGroup) + def cheatLambda(self, amount): return lambda: self.cheatMoney(amount) diff --git a/resources/ui/misc/light/plugins.png b/resources/ui/misc/light/plugins.png new file mode 100644 index 00000000..568d9e95 Binary files /dev/null and b/resources/ui/misc/light/plugins.png differ diff --git a/resources/ui/misc/light/pluginsoptions.png b/resources/ui/misc/light/pluginsoptions.png new file mode 100644 index 00000000..b9090ea2 Binary files /dev/null and b/resources/ui/misc/light/pluginsoptions.png differ