added a customizable plugin system

- the base LUA functionality has been implemented as a mandatory plugin
- the jtacautolase functionality has been implemented as a plugin
- added a VEAF framework plugin

The plugins have GUI elements in the Settings window.
This commit is contained in:
David Pierron 2020-10-12 17:27:13 +02:00
parent c77bfe9da2
commit d22943d755
20 changed files with 330 additions and 65 deletions

3
.gitignore vendored
View File

@ -21,4 +21,5 @@ logs/
qt_ui/logs/liberation.log qt_ui/logs/liberation.log
*.psd *.psd
resources/scripts/plugins/* plugin/custom/__plugins.lst
plugin/custom/*.lua

3
.gitmodules vendored
View File

@ -2,3 +2,6 @@
path = pydcs path = pydcs
url = https://github.com/pydcs/dcs url = https://github.com/pydcs/dcs
branch = master branch = master
[submodule "plugin/veaf"]
path = plugin/veaf
url = https://github.com/VEAF/dcs-liberation-veaf-framework

View File

@ -31,7 +31,7 @@ from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator
from theater import ControlPoint from theater import ControlPoint
from .. import db from .. import db
from ..debriefing import Debriefing from ..debriefing import Debriefing
from plugin import BasePlugin, INSTALLED_PLUGINS
class Operation: class Operation:
attackers_starting_position = None # type: db.StartingPosition attackers_starting_position = None # type: db.StartingPosition
@ -75,6 +75,7 @@ class Operation:
self.departure_cp = departure_cp self.departure_cp = departure_cp
self.to_cp = to_cp self.to_cp = to_cp
self.is_quick = False self.is_quick = False
self.listOfPluginsScripts = []
def units_of(self, country_name: str) -> List[UnitType]: def units_of(self, country_name: str) -> List[UnitType]:
return [] return []
@ -133,6 +134,36 @@ class Operation:
else: else:
self.defenders_starting_position = None 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): def generate(self):
radio_registry = RadioRegistry() radio_registry = RadioRegistry()
tacan_registry = TacanRegistry() tacan_registry = TacanRegistry()
@ -434,67 +465,12 @@ dcsLiberation.TargetPoints = {
trigger.add_action(DoScript(String(lua))) trigger.add_action(DoScript(String(lua)))
self.current_mission.triggerrules.triggers.append(trigger) self.current_mission.triggerrules.triggers.append(trigger)
# Inject Plugins Lua Scripts # Inject Plugins Lua Scripts and data
listOfPluginsScripts = [] self.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 Mist Script if not done already in the plugins for plugin in INSTALLED_PLUGINS:
if not "mist.lua" in listOfPluginsScripts and not "mist_4_3_74.lua" in listOfPluginsScripts: # don't load the script twice plugin.injectScripts(self)
trigger = TriggerStart(comment="Load Mist Lua framework") plugin.injectConfiguration(self)
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)
self.assign_channels_to_flights(airgen.flights, self.assign_channels_to_flights(airgen.flights,
airsupportgen.air_support) airsupportgen.air_support)

View File

@ -40,4 +40,7 @@ class Settings:
self.perf_culling = False self.perf_culling = False
self.perf_culling_distance = 100 self.perf_culling_distance = 100
# LUA Plugins system
self.plugins = {}

View File

@ -205,7 +205,7 @@ class PackageBuilder:
airfield, aircraft = assignment airfield, aircraft = assignment
flight = Flight(aircraft, plan.num_aircraft, airfield, plan.task) flight = Flight(aircraft, plan.num_aircraft, airfield, plan.task)
self.package.add_flight(flight) self.package.add_flight(flight)
flight.targetPoint = location flight.targetPoint = self.package.target
return True return True
def build(self) -> Package: def build(self) -> Package:
@ -218,7 +218,7 @@ class PackageBuilder:
for flight in flights: for flight in flights:
self.global_inventory.return_from_flight(flight) self.global_inventory.return_from_flight(flight)
self.package.remove_flight(flight) self.package.remove_flight(flight)
flight.targetPoint = None flight.targetPoint = None
class ObjectiveFinder: class ObjectiveFinder:
"""Identifies potential objectives for the mission planner.""" """Identifies potential objectives for the mission planner."""

10
plugin/__init__.py Normal file
View File

@ -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()
]

41
plugin/base_plugin.py Normal file
View File

@ -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]

View File

@ -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")

View File

@ -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

1
plugin/veaf Submodule

@ -0,0 +1 @@
Subproject commit 219cdffef087660fe448a41e1f187c4856e9d80f

90
plugin/veaf_plugin.py Normal file
View File

@ -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")

View File

@ -99,6 +99,8 @@ def load_icons():
ICONS["Generator"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/generator.png") ICONS["Generator"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/generator.png")
ICONS["Missile"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/missile.png") ICONS["Missile"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/missile.png")
ICONS["Cheat"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/cheat.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["TaskCAS"] = QPixmap("./resources/ui/tasks/cas.png")
ICONS["TaskCAP"] = QPixmap("./resources/ui/tasks/cap.png") ICONS["TaskCAP"] = QPixmap("./resources/ui/tasks/cap.png")

View File

@ -11,7 +11,7 @@ from game.game import Game
from game.infos.information import Information from game.infos.information import Information
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.finances.QFinancesMenu import QHorizontalSeparationLine from qt_ui.windows.finances.QFinancesMenu import QHorizontalSeparationLine
from plugin import BasePlugin, INSTALLED_PLUGINS
class QSettingsWindow(QDialog): class QSettingsWindow(QDialog):
@ -52,10 +52,22 @@ class QSettingsWindow(QDialog):
cheat.setEditable(False) cheat.setEditable(False)
cheat.setSelectable(True) 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.categoryList.setIconSize(QSize(32, 32))
self.categoryModel.appendRow(difficulty) self.categoryModel.appendRow(difficulty)
self.categoryModel.appendRow(generator) self.categoryModel.appendRow(generator)
self.categoryModel.appendRow(cheat) self.categoryModel.appendRow(cheat)
self.categoryModel.appendRow(plugins)
self.categoryModel.appendRow(pluginsOptions)
self.categoryList.setSelectionBehavior(QAbstractItemView.SelectRows) self.categoryList.setSelectionBehavior(QAbstractItemView.SelectRows)
self.categoryList.setModel(self.categoryModel) self.categoryList.setModel(self.categoryModel)
@ -65,10 +77,13 @@ class QSettingsWindow(QDialog):
self.initDifficultyLayout() self.initDifficultyLayout()
self.initGeneratorLayout() self.initGeneratorLayout()
self.initCheatLayout() self.initCheatLayout()
self.initPluginsLayout()
self.right_layout.addWidget(self.difficultyPage) self.right_layout.addWidget(self.difficultyPage)
self.right_layout.addWidget(self.generatorPage) self.right_layout.addWidget(self.generatorPage)
self.right_layout.addWidget(self.cheatPage) 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.addWidget(self.categoryList, 0, 0, 1, 1)
self.layout.addLayout(self.right_layout, 0, 1, 5, 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.moneyCheatBoxLayout.addWidget(btn, i/2, i%2)
self.cheatLayout.addWidget(self.moneyCheatBox, 0, 0) 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): def cheatLambda(self, amount):
return lambda: self.cheatMoney(amount) return lambda: self.cheatMoney(amount)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB