Skip to main content Link Menu Expand (external link) Document Search Copy Copied

Write your own plugin

In case you want to write your own plugin, there is a sample in the plugins/samples subdirectory, that will guide you through the steps. If you want your plugin to be added to the distribution, just contact me via the contact details below.

Plugin Structure

|_ db               
   |_ tables.sql        => DDLs for plugin-specific tables
   |_ update_v1.0.sql   => Update script, only needed if database changes have to be made
|_ lua
   |_ commands.lua      => Commands to be provided in the Hook environment
   |_ callbacks.lua     => Usual Hook callbacks for DCS (aka onXXX())
   |_ mission.lua       => lua file to be loaded inside the mission (no auto loading!)
|_ reports              => Reports used by the plugin (see ReportFramwork below)
__init__.py             => Package definition (see below)
commands.py             => Contains all Discord commands
listener.py             => Event listener for DCS events
version.py              => Holds the plugins version
README.md               => Each plugin should have a documentation

Configuration

Each plugin can use a json file to keep its config parameters. There json files are stored in ./config and it is a good habit to provide a sample for it. As each plugin might need a different configuration for each server and maybe some default configuration, the layout of the config files is as follows:

{
  "configs": [
    {
      "name": "I am the default section"
    },
    {
      "installation": "DCS.openbeta_server",
      "name": "I am a server specific section"
    }
  ]
}

To access the configuration, you can use

    config: dict = self.get_config(server)

in your Plugin implementation or

    config: dict = self.plugin.get_config(server)

in your EventListener implementation.

The default configuration will be merged with the respective server specific configuration, giving the server specific configuration the priority over the default. If you don’t want it like that, you need to overwrite the get_config() method in your own plugin configuration (see Punishment).

Classes

When implementing a plugin, there are some python classes that you need to know.

Class: Plugin

Base class for all plugins. Needs to be implemented inside the commands.py file (see below).

You have access to the following class variables:

  • self.plugin_name: Plugin name (“sample”)
  • self.plugin_version: Plugin version (“1.0”)
  • self.bot: Main DCSServerBot instance
  • self.log: Logging
  • self.pool: Database pool
  • self.loop: asyncio event loop
  • self.locals: dict from config.json
  • self.eventlistener: the EventListener instance bound to this plugin (optional)
from core import Plugin

class Sample(Plugin):
    def __init__(self, bot: DCSServerBot, eventlistener: Type[TEventListener] = None):
        super().__init__(bot, eventlistener)
        # do something when the plugin is initialized
    
    async def cog_unload(self):
        super.cog_unload()
        # do something when the plugin is unloaded

    def install(self):
        super.install()
        # do something when the plugin is installed for the first (!) time

    def migrate(self, version: str) -> None:
        # do something when the plugin is migrated (see below)
        pass

    async def before_dcs_update(self) -> None:
        # do something before a DCS upgrade takes place
        pass

    async def after_dcs_update(self) -> None:
        # do something after a DCS upgrade took place and before the servers are started
        pass

    async def prune(self, conn, *, days: int = 0, ucids: list[str] = None) -> None:
        # cleanup (the database) with data older than days and/or for specific users (ucids) 
        pass

    def rename(self, old_name: str, new_name: str) -> None:
        # called when a server rename took place and to be used to update your database tables accordingly
        pass

None of these methods needs to be overloaded for a plugin to work.

Class: EventListener

You have access to the following class variables:

  • self.plugin: the Plugin implementation bound to this EventListener
  • self.plugin_name: name of the plugin
  • self.bot: Main DCSServerBot instance
  • self.log: Logging
  • self.pool: Database pool
  • self.loop: asyncio event loop
  • self.locals: dict from config.json
  • self.commands: a list of commands implemented by this listener (autofilled)
from core import EventListener, Server, Plugin, Player

class SampleEventListener(EventListener):
    def __init__(self, plugin: Plugin):
        super().__init__(plugin)
        # do something when the listener is initialized
        
    async def registerDCSServer(self, data: dict) -> None:
        # called, when a DCS server is found and initialized
        # dict contains a dictionary with a lot of server information, like name, mission, active players,
        # weather and whatnot.
    
    # the following callbacks are derived from the Hooks environment:
    async def onMissionLoadBegin(self, data: dict) -> None:
        pass

    async def onMissionLoadEnd(self, data: dict) -> None:
        pass
    
    async def onSimulationStart(self, data: dict) -> None:
        pass
    
    async def onSimulationStop(self, data: dict) -> None:
        pass
    
    async def onSimulationPause(self, data: dict) -> None:
        pass
    
    async def onSimulationResume(self, data: dict) -> None:
        pass
    
    async def onPlayerConnect(self, data: dict) -> None:
        pass
    
    async def onPlayerStart(self, data: dict) -> None:
        pass
    
    async def onPlayerStop(self, data: dict) -> None:
        pass
    
    async def onPlayerChangeSlot(self, data: dict) -> None:
        pass
    
    async def onGameEvent(self, data: dict) -> None:
        pass
    
    async def onChatMessage(self, data: dict) -> None:
        pass

    # onChatCommand is called, whenever a command is fired in the ingame-chat
    async def onChatCommand(self, data: dict) -> None:
        server: Server = self.bot.servers[data['server_name']]
        player: Player = server.get_player(id=data['from_id'])
        if data['subcommand'] == 'atis':
            # can be used by everyone ...
            pass
        elif data['subcommand'] == 'restart' and player.has_discord_roles(['DCS Admin']):
            # can only be used by the DCS Admin role ...
            pass

    async def myCustomCommand(self, data: dict) -> None:
        # any custom command, which can be sent from the lua environment by using
        # local msg = {}
        # msg.command = 'myCustomCommand'
        # msg.param = { ... } -- all that goes into the dict
        # dcsbot.sendBotTable(msg)

Main Files

commands.py

This is the entry point for any Discord command. As DCSServerBot can be installed in multiple locations serving a bunch of DCS servers, you might end up with more than one active bot in your discord. To separate them, commands are usually called in the admin channels of each server (see examples below). Sometimes commands needs to query the whole database or must only be run on exactly one instance, the so-called MASTER instance. For this case, you already need to take care of the MASTER / AGENT split in your Discord hook. For how to handle Discord commands, see discord.py.

import platform
from core import Plugin, utils, Server, DCSServerBot
from discord.ext import commands
from .listener import SampleEventListener

class SampleAgent(Plugin):
    @commands.command(description='Agent-command', usage='<text>')
    @utils.has_role('DCS')
    @commands.guild_only()
    async def sample(self, ctx, text: str):
        server: Server = await self.bot.get_server(ctx)
        if server:
            await ctx.send(f'Server {server.name} is {server.status.name}!')
        else:
            # just for documentation purposes, Agents, only implement server-specific commands!
            pass

class SampleMaster(SampleAgent):
    @commands.command(description='Master-only command')
    @utils.has_role('DCS')
    @commands.guild_only()
    async def master(self, ctx):
        await ctx.send(f'This command is running on node {platform.node()}')

async def setup(bot: DCSServerBot):
    if bot.config.getboolean('BOT', 'MASTER') is True:
        await bot.add_cog(SampleMaster(bot, SampleEventListener))
    else:
        await bot.add_cog(SampleAgent(bot, SampleEventListener))

You don’t necessarily need to implement the MASTER / AGENT construct, if you only have commands that are to be run on agents.

listener.py

This is just the implementation of the EventListener class (see above).

callbacks.lua

Every plugin can have their own DCS World hook, that will be automatically added to the Scripts\Hooks environment. To achieve this, you just need to place a file named callbacks.lua in your lua directory.

local dcsbot	= base.dcsbot

local myplugin = myplugin or {} 

--[[
If you want to dynamically load some lua into your mission, you do this in your onMissionLoadEnd hook.
Best is to load a file name mission.lua, to have some kind of naming standard, but you can name it
as you like.
The base commands of DCSServerBot are loaded into the mission environment by the bot already, so you have
some commands available that you can use (see mission.lua). 
]]
function myplugin.onMissionLoadEnd()
    log.write('DCSServerBot', log.DEBUG, 'MyPlugin: onMissionLoadEnd()')
    net.dostring_in('mission', 'a_do_script("dofile(\\"' .. lfs.writedir():gsub('\\', '/') .. 'Scripts/net/DCSServerBot/myplugin/mission.lua' .. '\\")")')
end

function myplugin.onPlayerConnect(id)
    local msg = {}
    msg.command = 'myCustomCommand'
    msg.id = id
    dcsbot.sendBotTable(msg)
end

DCS.setUserCallbacks(myplugin)

commands.lua

If you want to send a command by the bot into the DCS Hooks environment, you implement the command in here. So if you for instance run .pause in Discord, this will result in a JSON message with “command”: “pauseMission” to DCS and call a function pauseMission() that is implemented in some commands.lua in one of the bots plugins.

local base = _G
local dcsbot = base.dcsbot

function dcsbot.pauseMission(json)
    log.write('DCSServerBot', log.DEBUG, 'Mission: pauseMission()') 
    DCS.setPause(true)
end

mission.lua

This is a special file and does not necessarily need to be named like this. But I would still recommend it, as you then know which of the lua files will be loaded into the mission environment (if you do so in your onMissionLoadEnd (see above)). The following commands can be used in the mission environment (ME):

function sendBotMessage(msg, channel) end
function sendBotTable(tbl, channel) end
function sendEmbed(title, description, img, fields, footer, channel) end
function updateEmbed(id, title, description, img, fields, footer, channel) end
function callback(msg, channel) end
function startMission(id) end
function restartMission() end
function disableUserStats() end

Data Classes

To ease the access to server, player and mission information and to run usual commands, DCSServerBot provides classes to do so.

Server

See server.py. You have usually two options to retrieve the running server instance:

a) Plugin

In your plugins, you usually want to run a Discord command that sends information to a specific server. To get the respective server, you can take advantage of the channel/server mapping that DCSServerBot implements in its configuration already. That means, that if you for instance run a command in the dedicated admin channel of any server, you can access the Server instance, directly through the Discord context:

async def sample(self, ctx, text: str):
    server: Server = await self.bot.get_server(ctx)

b) EventListener

In your EventListener, you receive commands from a DCS server. And sure - that server tells you its name with every command it sends to you:

async def onChatCommand(self, data: dict) -> None:
    server: Server = self.bot.servers[data['server_name']]

Mission

The running mission (mission.py) can be accessed through the Server instance like so:

mission: Mission = server.current_mission

Player

There are several ways to access a player (player.py):

  • by their UCID
  • by their Discord ID (if they are a Discord member and properly mapped)
  • by their in-game ID (1, 2, 3, …)
  • by their in-game name (which is fortunately unique per session)

This can be achieved by asking your server about the player and providing the relevant parameter to the get_player() method:

async def onChatCommand(self, data: dict) -> None:
    server: Server = self.bot.servers[data['server_name']]
    player: Player = server.get_player(id=data['from_id'])

discord.Member

As DCSServerBot stores a link between DCS players and Discord members, you surely can access the discord member information, too:

async def onChatCommand(self, data: dict) -> None:
    server: Server = self.bot.servers[data['server_name']]
    player: Player = server.get_player(id=data['from_id'])
    if player.member:
        self.log.info(f"Player {player.name} is member {player.member.display_name}!")

Reports

See Report Framework.

Versioning

Every plugin has its own version. Versioning starts with a file named version.py like so:

version.py:

__version__ = "1.0"

You only want to change the plugins version, if a change to the underlying database has taken place or if some other migration is needed. You can express major changes by version number changes, too, but this is not a must.

Database Handling

DCSServerBot uses a PostgreSQL database to hold all tables, stored procedures and whatnot. Every plugin can create its own database elements. To do so, you need to add the DDL, line by line in a file named tables.sql below the optional “db” directory.

tables.sql:

CREATE TABLE IF NOT EXISTS bans (ucid TEXT PRIMARY KEY, banned_by TEXT NOT NULL, reason TEXT, banned_at TIMESTAMP NOT NULL DEFAULT NOW());

To access the database, you should use the database pool that is available in every common framework class:

conn = self.pool.getconn()
try:
    with closing(conn.cursor()) as cursor:
        cursor.execute('INSERT INTO bans (ucid, banned_by, reason) VALUES (%s, %s, %s) ON CONFLICT DO NOTHING',
                       (player.ucid, self.plugin_name, reason))
        conn.commit()
except (Exception, psycopg2.DatabaseError) as error:
    conn.rollback()
    self.log.exception(error)
finally:
    self.pool.putconn(conn)

Auto-Migration

DCSServerBot was invented to ease the life of DCS server admins. That said, you should take care of your fellow admins and create code that migrates database tables / entries or any config file that needs to be amended automatically. There are lots of little helpers in the DCSServerBot framework to do so.

Whenever a version of a plugin changes (version.py), DCSServerBot runs several update mechanisms, that you can implement if necessary:

Database Table Migration

Just implement a script named “update_vX.Y.sql”, where X.Y is there version where you want to migrate FROM. To migrate the database from plugin vesion 1.0 to 1.1, you need to implement a script named update_v1.0.sql.

update_v1.0.sql:

ALTER TABLE bans ADD COLUMN (test TEXT NOT NULL DEFAULT 'n/a');

Any Other Migration

Each Plugin can implement the method

    def migrate(self, version: str) -> None:
        pass

that will take care of anything that needs to be done when migrating TO version version.