SPADS_plugin_development_(Python) - beyond-all-reason/springrts_engine_wiki_mirror GitHub Wiki
SPADS is an autohost system coded in Perl, it supports both Perl and Python plugins. This tutorial deals with Python plugin development only (for Perl plugin development another tutorial is available here).
SPADS plugins are object-oriented modules. It is strongly recommended to already know Python basics before starting Python plugin development (a Python tutorial is available here).
= Technical prerequisites =
Python (the Python interpreter and its libraries) must be installed on the system (versions 2.6+ or 3.2+ recommended). The type of installation (32-bit / 64-bit) must be consistent with the Perl version and the Spring engine used for SPADS.
Additionally, in order to allow communication between Perl code (SPADS
core) and Python code (SPADS plugins), SPADS uses a Perl module named
Inline::Python
which must also be installed on the system. It is
recommended to install the latest version for SPADS, refer to
[https://springrts.com/wiki/SPADS_Inline::Python_installation_guide
this guide] for installation.
In this section we are going to write our first SPADS plugin. So let's start simple with a good old "Hello world". The plugin will answer "Hello World" to anyone saying "Hello" in a private message to SPADS.
Some plugin templates are available to help you bootstrap your plugin
development. The commented versions of these templates are available
here,
while the raw versions (without comment) are available
here (the
.pm
files must be ignored for this tutorial, these are the Perl
versions of the templates).
Our first plugin will be very basic, so we will use the simplest
template: mysimpleplugin.py
. Let's download the commented version of
this
template
and open it with our favorite editor. As you can see, the code is
heavily commented (actually every single line of code is explained), so
I won't go into further details here. Now that you've read and
understood the commented template, let's actually start plugin
development.
First we must name our plugin. Let's call it HelloWorld
. We have to
rename the downloaded template from mysimpleplugin.py
to
helloworld.py
, and edit it to replace MySimplePlugin
by HelloWorld
in the source code.
Before going into actually writing new code, let's just check that the
plugin works in SPADS (if not done yet, you have to configure the SPADS
plugins
directory
in your spads.conf
configuration file and reload SPADS configuration).
We have to move our new plugin (helloworld.py
) in the SPADS plugins
directory so that SPADS can find it. Then, as a privileged SPADS user,
we can type following command (in a private message to SPADS for
example): !plugin HelloWorld load
. SPADS should answer Loaded plugin HelloWorld.
, which indicates the plugin has been loaded successfully.
If all is ok, we can let this SPADS instance running like this, we will get back to it later.
Writing plugin code mainly consists in implementing plugin callbacks and calling plugin API functions, as specified in SPADS plugin API documentation.
So we want our plugin to react to some private messages sent to SPADS. To do so we have to implement a plugin callback function which is called by SPADS core each time a lobby private message is received: this is the onPrivateMsg event-based callback.
We also want our plugin to send a private message, to do so we can call the plugin API function sayPrivate.
Now that we have identified the API functions that we need, we can
actually implement our plugin. Here is a commented implementation
example of the onPrivateMsg
callback, which will answer Hello World
to anyone saying Hello
in a private message to SPADS:
# This is the callback called each time SPADS receives a private message
# self is the plugin object (first parameter of all plugin callbacks)
# userName is the name of the user sending the private message
# message is the message sent by the user
def onPrivateMsg(self,userName,message):
# Here we "fix" strings received from Perl in case
# the Inline::Python module transmits them as byte strings
(userName,message)=spads.fix_string(userName,message)
# We check the message sent by the user is "Hello"
if message == 'Hello':
# We send our wonderful Hello World message
spads.sayPrivate(userName,'Hello World')
# We return 0 because we don't want to filter out private messages
# for other SPADS processing
return 0
All we have to do now is adding this callback declaration in our
helloworld.py
file located in SPADS plugins directory. We obtain this
fully functional HelloWorld
plugin.
Let's test this plugin in SPADS. First, since we modified the plugin
source code, we have to tell SPADS to reload the plugin as follows:
!plugin HelloWorld reload
. SPADS should answer Reloaded plugin HelloWorld.
, which indicates the plugin has been reloaded successfully.
Finally, just say Hello
to SPADS in a private message. Congratulations
for your first SPADS plugin! ;)
= Configurable plugin tutorial (ForbiddenWords) =
In this section we are going to write our first configurable SPADS
plugin (a configurable plugin has its own configuration file, named
after the plugin name but with .conf
extension). This plugin will
monitor all messages said by players in the battle lobby, and will kick
players who use swear words.
The first step of writing a configurable plugin is to choose how we will
configure it. In our example, we will use 2 configuration parameters:
words
will contain the list of forbidden words, and immuneLevel
will
contain the minimum autohost access level to be immune regarding these
forbidden words checks.
We choose to make words
a global setting (unmodifiable, not impacted
by preset change), and immuneLevel
a preset setting (modifiable, can
be impacted by preset change). The words
global setting will have no
restriction (can be empty, can contain any character...), whereas the
immuneLevel
preset setting will only be allowed to be an integer or
integer range.
Once we have a clear view of our configuration settings, we can start adapting the template for our needs. Since we are making a configurable plugin, this time we will download the configurable plugin template and its associated configuration file example.
First, let's take a look at the configuration file example. This is a
just a basic configuration file containing one global setting example
and one preset setting example. Let's modify this file to match the
configuration we chose as follows (don't forget to rename the file from
MyConfigurablePlugin.conf
to ForbiddenWords.conf
also):
|
Then we must prepare the plugin template itself, by renaming it from
myconfigurableplugin.py
to forbiddenwords.py
and editing it to
replace MyConfigurablePlugin
by ForbiddenWords
in the source code.
We must also adapt the template so that it uses the configuration
settings we chose. This is done by modifying the globalPluginParams
and presetPluginParam
declarations as follows:
# We define one global setting "words" and one preset setting "immuneLevel".
# "words" has no type associated (no restriction on allowed values)
# "immuneLevel" must be an integer or an integer range
# (check %paramTypes hash in SpadsConf.pm for a complete list of allowed
# setting types)
globalPluginParams = { 'words': [] }
presetPluginParams = { 'immuneLevel': ['integer','integerRange'] }
We want our plugin to react to messages said in the battle lobby. There is no dedicated callback for this event in SPADS plugin API documentation, so we have to set up our own handler on the SAIDBATTLE lobby command. To do so we have to call the plugin API function addLobbyCommandHandler. We will call this function at the end of our plugin constructor as follows, so that it will be set up directly when the plugin is loaded:
[...]
# We set up a lobby command handler on SAIDBATTLE
spads.addLobbyCommandHandler({'SAIDBATTLE': hLobbySaidBattle})
However, if SPADS is disconnected from the lobby due to network problems for example, all lobby handlers are automatically removed. So we must re-add them each time we connect to lobby server. This can be done using the onLobbyConnected event-based callback as follows:
# This callback is called each time we (re)connect to the lobby server
def onLobbyConnected(self,lobbyInterface):
# When we are disconnected from the lobby server, all lobby command
# handlers are automatically removed, so we (re)set up our command
# handler here.
spads.addLobbyCommandHandler({'SAIDBATTLE': hLobbySaidBattle})
Also, it is a good practice to remove any handler we have added when the plugin is unloaded. To do so we must implement the onUnload event-based callback as follows:
# This callback is called when the plugin is unloaded
def onUnload(self,reason):
# We remove our lobby command handler when the plugin is unloaded
spads.removeLobbyCommandHandler(['SAIDBATTLE'])
Finally, we have to implement our SAIDBATTLE
handler
hLobbySaidBattle
. In this handler we need to perform following
operations:
- skip processing if the user is the autohost itself: we need to access SPADS configuration to compare the user name with the lobbyLogin setting value. So we will need the plugin API function getSpadsConf.
- perform processing according to our configuration (
words
andimmuneLevel
settings): we need to access our plugin configuration, so we will need the plugin API function getPluginConf. - retrieve the autohost access level of the user: we will use the plugin API function getUserAccessLevel
- send a message to the battle lobby when we kick someone: we will use the plugin API function sayBattle.
- send a KICKFROMBATTLE lobby command to kick a user: we will use the plugin API function queueLobbyCommand.
Here is a commented implementation example of this hLobbySaidBattle
handler, which will kick any non-privileged user saying a forbidden word
in the battle lobby:
# This is the handler we set up on SAIDBATTLE lobby command.
# It is called each time a player says something in the battle lobby.
# command is the lobby command name (SAIDBATTLE)
# user is the name of the user who said something in the battle lobby
# message is the message said in the battle lobby
def hLobbySaidBattle(command,user,message):
# First we "fix" strings received from Perl in case
# the Inline::Python module transmits them as byte strings
(user,message)=spads.fix_string(user,message)
# Here we check it's not a message from SPADS (so we don't kick ourself)
spadsConf = spads.getSpadsConf()
if user == spadsConf['lobbyLogin']:
return
# Then we check the user isn't a privileged user
# (autohost access level >= immuneLevel)
pluginConf = spads.getPluginConf()
if int(spads.getUserAccessLevel(user)) >= int(pluginConf['immuneLevel']):
return
# We put the forbidden words in a list
forbiddenWords = pluginConf['words'].split(';')
# We test each forbidden word
for forbiddenWord in forbiddenWords:
# If the message contains the forbidden word (case insensitive)
if re.search(r'\b' + re.escape(forbiddenWord) + r'\b',message,re.IGNORECASE):
# Then we kick the user from the battle lobby
spads.sayBattle("Kicking %s from battle (watch your language!)" % user)
spads.queueLobbyCommand(["KICKFROMBATTLE",user])
# We quit the foreach loop (no need to test other forbidden word)
break
As we are using regular expressions in our plugin code, we must not
forget to import the re
Python module at the start of our plugin:
# Import the regular expression module to check for forbidden words
import re
Once we put all that together, we obtain this fully functional ForbiddenWords plugin and its associated configuration file.
To test our plugin, we have to put the plugin module in SPADS plugins directory, and the associated configuration file in SPADS etc directory.
Then we load the plugin as follows: !plugin ForbiddenWords load
. And
finally, as an unprivileged user we can try to say some forbidden words
in the battle lobby and we should get kicked by the plugin.
In this section we are going to write a plugin which implements a new
command for SPADS. Such plugins are configurable plugins like the one we
wrote just before, with 2 additional files to configure the new
commands. This plugin will give current time when someone types !time
.
This time we need to download 4 files to prepare our plugin: the new-command plugin template, the associated configuration file example, the help file example and the commands rights configuration file example.
As usual, we rename these files to match our plugin name:
MyNewCommandPlugin
--> TimePlugin
. Then we edit the plugin template
timeplugin.py
and replace MyNewCommandPlugin
by TimePlugin
in the
source code, and we do the same for the plugin configuration template
TimePlugin.conf
, so that the commandsFile
and helpFile
settings
are consistent with the files we just renamed.
Then we can edit our command rights requirements configuration file:
TimePluginCmd.conf
. This file uses the same syntax as the standard
SPADS commands.conf
file. The template provides a default command
myCommand
with no requirement. We will just rename this command to
time
:
|
Now we need to write the help information for our new command. This is
done in the file TimePluginHelp.dat
. This file uses the same syntax as
the standard SPADS help.dat
file, and the template provides a help
example for a command myCommand
as a syntax reminder. After each
command declaration, the first line is the command syntax description,
and the other lines are optional usage examples. Let's replace this help
example with our own help information for our basic !time
command:
|
The new-command plugin template that we used to initialize our plugin
already defines a new command named myCommand
, so all we have to do is
to edit our plugin file ( timeplugin.py
) and replace this command by
our own time
command:
First, let's modify the code which sets up the new SPADS command handler using the plugin API function addSpadsCommandHandler. This call is located in the plugin constructor. We edit it so that it becomes:
[...]
# We declare our new command and the associated handler
spads.addSpadsCommandHandler({'time': hSpadsTime})
[...]
We must modify the same way the code which removes this SPADS handler using the plugin API function removeSpadsCommandHandler in the onUnload event-based callback:
[...]
# We remove our new command handler
spads.removeSpadsCommandHandler(['time'])
[...]
Finally, we must replace the handler example hMyCommand
by our own
handler hSpadsTime
. In order to answer to the user issuing the
command, we can use the plugin API function
answer,
which will send an answer message to the user in the same way he sent
the command (private message, battle lobby...).
Here is an implementation example for this basic command:
# This is the handler for our new command
def hSpadsTime(source,user,params,checkOnly):
# checkOnly is true if this is just a check for callVote command, not a real command execution
if checkOnly :
# time is a basic command, we have nothing to check in case of callvote
return 1
# We get current time using "now" function of datetime class from datetime module
current_time = datetime.datetime.now()
current_time_string = current_time.strftime("%H:%M:%S")
# We call the API function "answer" to send back the response to the user who called the command
# using same canal as he used (private message, battle lobby, in game message...)
spads.answer("Current local time: %s" % current_time_string)
As we are retrieving current time in our plugin code, we must not forget
to import the datetime
Python module at the start of our plugin:
# Import the datetime module so we can get current time for our !time command
import datetime
Once we put all that together, we obtain this fully functional TimePlugin plugin, and its associated configuration file, command rights configuration file and command help file.
To test our plugin, we have to put the plugin module and the plugin help file in SPADS plugins directory, and the 2 configuration files in SPADS etc directory.
Then we load the plugin as follows: !plugin TimePlugin load
. And
finally, we can try to say !time in a private message to SPADS or in
the battle lobby, and SPADS should answer giving current local time. Our
!time
command help should also appear in !help
and !help time
outputs.
Category:SPADS