Document Lua plugin APIs.

Trying to fix the singleton-ness in the plugin manager because it
prevents injecting settings until the game is fully committed (new game
wizard completed). Added the docs describing what I think I've been able
to discover.
This commit is contained in:
Dan Albert 2023-04-25 20:26:28 -07:00
parent b6059f692e
commit 664efa3ace
3 changed files with 64 additions and 3 deletions

View File

@ -4,7 +4,7 @@ import logging
import os
from abc import ABC, abstractmethod
from pathlib import Path
from typing import TYPE_CHECKING, Optional
from typing import Optional, TYPE_CHECKING
from dcs import Mission
from dcs.action import DoScript, DoScriptFile
@ -17,7 +17,6 @@ from game.plugins import LuaPluginManager
from game.theater import TheaterGroundObject
from game.theater.iadsnetwork.iadsrole import IadsRole
from game.utils import escape_string_for_lua
from .missiondata import MissionData
if TYPE_CHECKING:
@ -207,16 +206,26 @@ class LuaGenerator:
self.mission.triggerrules.triggers.append(trigger)
def inject_lua_trigger(self, contents: str, comment: str) -> None:
"""Creates the trigger for running the text script at mission start."""
trigger = TriggerStart(comment=comment)
trigger.add_action(DoScript(String(contents)))
self.mission.triggerrules.triggers.append(trigger)
def bypass_plugin_script(self, mnemonic: str) -> None:
"""Records a script has having been intentionally ignored.
It's not clear why this is needed. It looks like this might be a holdover from
when mission generation was driven by a singleton and we needed to avoid
double-loading plugins if the generator ran twice (take off, cancel, take off)?
For now, this prevents duplicates from being handled twice.
"""
self.plugin_scripts.append(mnemonic)
def inject_plugin_script(
self, plugin_mnemonic: str, script: str, script_mnemonic: str
) -> None:
""" "Creates a trigger for running the script file at mission start."""
if script_mnemonic in self.plugin_scripts:
logging.debug(f"Skipping already loaded {script} for {plugin_mnemonic}")
return

View File

@ -14,6 +14,19 @@ if TYPE_CHECKING:
class LuaPluginWorkOrder:
"""A script to be loaded at mision start.
Typically, a work order is used for the main plugin script, and another for
configuration. The main script is added to scriptsWorkOrders and the configuration
to configurationWorkOrders. As far as I can tell, there's absolutely no difference
between those two lists and that could be merged.
Other scripts can also be run by being added to either of these lists.
A better name for this is probably just "LuaPluginScript", since that appears to be
all they are.
"""
def __init__(
self, parent_mnemonic: str, filename: str, mnemonic: str, disable: bool
) -> None:
@ -23,6 +36,7 @@ class LuaPluginWorkOrder:
self.disable = disable
def work(self, lua_generator: LuaGenerator) -> None:
"""Inject the script for this work order into the mission, or ignores it."""
if self.disable:
lua_generator.bypass_plugin_script(self.mnemonic)
else:
@ -32,6 +46,8 @@ class LuaPluginWorkOrder:
class PluginSettings:
"""A common base for plugin configuration and per-plugin option configuration."""
def __init__(self, identifier: str, enabled_by_default: bool) -> None:
self.identifier = identifier
self.enabled_by_default = enabled_by_default
@ -57,6 +73,8 @@ class PluginSettings:
class LuaPluginOption(PluginSettings):
"""A boolean option for the plugin."""
def __init__(self, identifier: str, name: str, enabled_by_default: bool) -> None:
super().__init__(identifier, enabled_by_default)
self.name = name
@ -64,6 +82,8 @@ class LuaPluginOption(PluginSettings):
@dataclass(frozen=True)
class LuaPluginDefinition:
"""Object mapping for plugin.json."""
identifier: str
name: str
present_in_ui: bool
@ -74,6 +94,7 @@ class LuaPluginDefinition:
@classmethod
def from_json(cls, name: str, path: Path) -> LuaPluginDefinition:
"""Loads teh plugin definitions from the given plugin.json path."""
data = json.loads(path.read_text())
options = []
@ -120,6 +141,22 @@ class LuaPluginDefinition:
class LuaPlugin(PluginSettings):
"""A Liberation lua plugin.
A plugin is a mod that is able to inject Lua code into the Liberation mission start
up. Some of these are bundled (Skynet, mist, EWRS, etc), but users can add their own
as well.
A plugin is defined by a plugin.json file in resources/plugins/<name>/plugin.json.
That file defines the name to be shown in the settings UI, whether it should be
enabled by default, the scripts to run, and (optionally) boolean options for
controlling plugin behavior.
The plugin identifier is defined by the name of the directory containing it.
Plugin options have their own set of default settings, UI names, and IDs.
"""
def __init__(self, definition: LuaPluginDefinition) -> None:
self.definition = definition
super().__init__(self.definition.identifier, self.definition.enabled_by_default)
@ -147,15 +184,22 @@ class LuaPlugin(PluginSettings):
return cls(definition)
def set_settings(self, settings: Settings) -> None:
"""Attaches the plugin to a settings object."""
super().set_settings(settings)
for option in self.definition.options:
option.set_settings(self.settings)
def inject_scripts(self, lua_generator: LuaGenerator) -> None:
"""Injects the plugin's scripts into the mission."""
for work_order in self.definition.work_orders:
work_order.work(lua_generator)
def inject_configuration(self, lua_generator: LuaGenerator) -> None:
"""Injects the plugin's options and configuration scripts into the mission.
It's not clear why the script portion of this needs to exist, and could probably
instead be the same as inject_scripts.
"""
# inject the plugin options
if self.options:
option_decls = []

View File

@ -4,11 +4,12 @@ from pathlib import Path
from typing import Dict, List
from game.settings import Settings
from .luaplugin import LuaPlugin
class LuaPluginManager:
"""Manages available and loaded lua plugins."""
_plugins_loaded = False
_plugins: Dict[str, LuaPlugin] = {}
@ -48,5 +49,12 @@ class LuaPluginManager:
@classmethod
def load_settings(cls, settings: Settings) -> None:
"""Attaches all loaded plugins to the given settings object.
The LuaPluginManager singleton can only be attached to a single Settings object
at a time, and plugins will update the Settings object directly, so attaching
the plugin manager to a detached Settings object (say, during the new game
wizard, but then canceling the new game) will break the settings UI.
"""
for plugin in cls.plugins():
plugin.set_settings(settings)