mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
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:
parent
4c394a9e2d
commit
8827f7df34
@ -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.
|
||||
|
||||
|
||||
@ -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
2
game/plugins/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from .luaplugin import LuaPlugin
|
||||
from .manager import LuaPluginManager
|
||||
180
game/plugins/luaplugin.py
Normal file
180
game/plugins/luaplugin.py
Normal 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
50
game/plugins/manager.py
Normal 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)
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
from .luaplugin import LuaPlugin
|
||||
from .manager import LuaPluginManager
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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)
|
||||
|
||||
|
||||
71
qt_ui/windows/settings/plugins.py
Normal file
71
qt_ui/windows/settings/plugins.py
Normal 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))
|
||||
@ -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
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
{
|
||||
"mnemonic": "base",
|
||||
"skipUI": true,
|
||||
"nameInUI": "",
|
||||
"defaultValue": true,
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
{
|
||||
"mnemonic": "jtacautolase",
|
||||
"nameInUI": "JTAC Autolase",
|
||||
"defaultValue": true,
|
||||
"specificOptions": [
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
{
|
||||
"mnemonic": "skynetiads",
|
||||
"nameInUI": "Skynet IADS",
|
||||
"defaultValue": false,
|
||||
"specificOptions": [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user