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
This commit is contained in:
Dan Albert 2020-10-24 00:34:23 -07:00
parent 4c394a9e2d
commit 8827f7df34
16 changed files with 357 additions and 341 deletions

View File

@ -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,12 +226,9 @@ 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.
# TODO: Remove in 2.3.

View File

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

2
game/plugins/__init__.py Normal file
View File

@ -0,0 +1,2 @@
from .luaplugin import LuaPlugin
from .manager import LuaPluginManager

180
game/plugins/luaplugin.py Normal file
View File

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

50
game/plugins/manager.py Normal file
View File

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

View File

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

View File

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

View File

@ -1,2 +0,0 @@
from .luaplugin import LuaPlugin
from .manager import LuaPluginManager

View File

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

View File

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

View File

@ -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,15 +98,15 @@ class QSettingsWindow(QDialog):
self.categoryModel.appendRow(cheat)
self.right_layout.addWidget(self.cheatPage)
self.initPluginsLayout()
if self.pluginsPage:
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)
if self.pluginsOptionsPage:
self.pluginsOptionsPage = PluginOptionsPage()
pluginsOptions = QStandardItem("LUA Plugins Options")
pluginsOptions.setIcon(CONST.ICONS["PluginsOptions"])
pluginsOptions.setEditable(False)
@ -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)

View File

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

View File

@ -16,7 +16,6 @@ The *base* and *jtacautolase* plugins both are included in the standard dcs-libe
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

View File

@ -1,5 +1,4 @@
{
"mnemonic": "base",
"skipUI": true,
"nameInUI": "",
"defaultValue": true,

View File

@ -1,5 +1,4 @@
{
"mnemonic": "jtacautolase",
"nameInUI": "JTAC Autolase",
"defaultValue": true,
"specificOptions": [

View File

@ -1,5 +1,4 @@
{
"mnemonic": "skynetiads",
"nameInUI": "Skynet IADS",
"defaultValue": false,
"specificOptions": [