SPADS plugin development (Python): Difference between revisions

From Spring
Jump to navigationJump to search
Bibim (talk | contribs)
Bibim (talk | contribs)
Replaced content with "This page is now hosted on GitHub: [https://github.com/Yaribz/SPADS/wiki/SPADS-plugin-development-(Python) SPADS plugin development (Python)]"
 
(44 intermediate revisions by the same user not shown)
Line 1: Line 1:
'''/!\ IN PROGRESS /!\'''
This page is now hosted on GitHub: [https://github.com/Yaribz/SPADS/wiki/SPADS-plugin-development-(Python) SPADS plugin development (Python)]
 
= Introduction =
 
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 [https://springrts.com/wiki/SPADS_plugin_development 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 [https://docs.python.org/3/tutorial/ 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 <code>Inline::Python</code> which must also be installed on the system. The current stable version of this module, which is installed on demand by most Linux distributions, suffers from a bug which makes Perl strings appear as byte strings in Python 3 code (Python 2 is unaffected). This is not a blocking problem, as Python can easily convert these data if needed and the plugin API offers dedicated functions to deal with the problem (cf the [http://planetspads.free.fr/spads/doc/spadsPluginApiDoc.html#Perl-strings-conversions PYTHON SPECIFIC NOTES - Perl strings conversions] section of the SPADS Plugin API documentation). However, a cleaner way to solve this problem is to install a fixed version of the module, directly from the official module maintainer repository. The remaining of this section will detail the different ways to install the <code>Inline::Python</code> prerequisite.
 
== Installing Inline::Python on Linux ==
Methods are listed in increasing order of complexity, but the recommended method is the last one, as it allows installing the latest version of <code>Inline::Python</code> which handles Perl strings correctly.
=== Method 1: Using the package manager of your Linux distribution ===
This is the easiest way to install <code>Inline::Python</code> (if your distribution supports it), as it automatically downloads and installs a package specifically prepared for your distribution. However, this method installs the stable release of <code>Inline::Python</code>, which suffers from the string conversion bug with Python 3 explained above. So this method is only recommended if you don't plan to use Python 3 or if other installation methods fail.
For this method you just need to find the name of the system package containing the <code>Inline::Python</code> Perl module and install it on your system.
 
For example, on Debian based distributions: <code>apt-get install libinline-python-perl</code><br>
On Red Hat based distributions: <code>dnf install perl-Inline-Python</code>
 
=== Method 2: Using cpan ===
<code>cpan</code>
 
=== Method 3: Using cpanminus ===
<code>cpanminus</code>
 
== Installing Inline::Python on Windows ==
 
= Simple plugin tutorial (HelloWorld) =
 
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.
 
== Choosing our template ==
 
Some plugin templates are available to help you bootstrap your plugin development. The commented versions of these templates are available [http://planetspads.free.fr/spads/plugins/templates/commented/ here], while the raw versions (without comment) are available [http://planetspads.free.fr/spads/plugins/templates/raw/ here].
 
Our first plugin will be very basic, so we will use the simplest template: "mysimpleplugin.py". Let's download [http://planetspads.free.fr/spads/plugins/templates/commented/mysimpleplugin.py 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.
 
== Adapting the template ==
 
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.
 
== Checking SPADS plugin system ==
 
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 [http://planetspads.free.fr/spads/doc/spadsDoc_All.html#global:pluginsDir 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 ==
 
Writing plugin code mainly consists in implementing plugin callbacks and calling plugin API functions, as specified in [http://planetspads.free.fr/spads/doc/spadsPluginApiDoc.html 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 [http://planetspads.free.fr/spads/doc/spadsPluginApiDoc.html#onPrivateMsg-self-userName-message onPrivateMsg] event-based callback.
 
We also want our plugin to send a private message, to do so we can call the plugin API function [http://planetspads.free.fr/spads/doc/spadsPluginApiDoc.html#sayPrivate-user-message 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:
<syntaxhighlight lang="python">
    # 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</syntaxhighlight>
 
All we have to do now is adding this callback declaration in our HelloWorld.pm file located in SPADS plugins directory. We obtain [http://planetspads.free.fr/spads/plugins/tutorials/HelloWorld.pm this fully functional HelloWorld plugin].
 
== Testing our 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.
 
== Specifying our configuration parameters ==
 
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.
 
== Preparing the template ==
 
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 [http://planetspads.free.fr/spads/plugins/templates/commented/MyConfigurablePlugin.pm the configurable plugin template] and its associated [http://planetspads.free.fr/spads/plugins/templates/commented/MyConfigurablePlugin.conf 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):
{|style="margin-top: 10px; margin-left: 40px; background-color: #38474E; border: 1px solid black; border-color: #20292E" |
|
  # This is our global setting (can't be changed without reloading the configuration)
  words:ass;asshole;bastard;bitch;cunt;fuck;motherfucker;shit;whore
 
  # We must define our preset setting in the default preset at least
  [default]
 
  # This is our preset setting, which can be changed with "!plugin ... set ..."
  # or by loading a preset.
  # "100" is the default value, and any integer between 0 and 140 is allowed
  immuneLevel:100|0-140
|}
Then we must prepare the plugin template itself, by renaming it from "MyConfigurablePlugin.pm" to "ForbiddenWords.pm" 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:
{|style="margin-top: 10px; margin-left: 40px; background-color: #38474E; border: 1px solid black; border-color: #20292E" |
|
  # 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)
  my %globalPluginParams = ( words => [] );
  my %presetPluginParams = ( immuneLevel => ['integer','integerRange'] );
|}
== Writing plugin code ==
 
We want our plugin to react to messages said in the battle lobby. There is no dedicated callback for this event in [http://planetspads.free.fr/spads/doc/spadsPluginApiDoc.html SPADS plugin API documentation], so we have to set up our own handler on the [http://springrts.com/dl/LobbyProtocol/ProtocolDescription.html#SAIDBATTLE:server SAIDBATTLE lobby command]. To do so we have to call the plugin API function [http://planetspads.free.fr/spads/doc/spadsPluginApiDoc.html#addLobbyCommandHandler-handlers-priority-caller 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:
{|style="margin-top: 10px; margin-left: 40px; background-color: #38474E; border: 1px solid black; border-color: #20292E" |
|
    [...]
   
    # We set up a lobby command handler on SAIDBATTLE
    addLobbyCommandHandler({SAIDBATTLE => \&hLobbySaidBattle});
   
    # We return the instantiated plugin
    return $self;
   
  }
|}
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 [http://planetspads.free.fr/spads/doc/spadsPluginApiDoc.html#onLobbyConnected-self-lobbyInterface onLobbyConnected] event-based callback as follows:
{|style="margin-top: 10px; margin-left: 40px; background-color: #38474E; border: 1px solid black; border-color: #20292E" |
|
  # This callback is called each time we (re)connect to the lobby server
  sub onLobbyConnected {
   
    # When we are disconnected from the lobby server, all lobby command
    # handlers are automatically removed, so we (re)set up our command
    # handler here.
    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 [http://planetspads.free.fr/spads/doc/spadsPluginApiDoc.html#onUnload-self-context onUnload] event-based callback as follows:
{|style="margin-top: 10px; margin-left: 40px; background-color: #38474E; border: 1px solid black; border-color: #20292E" |
|
  # This callback is called when the plugin is unloaded
  sub onUnload {
   
    # We remove our lobby command handler when the plugin is unloaded
    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 [http://planetspads.free.fr/spads/doc/spadsDoc_All.html#global:lobbyLogin lobbyLogin] setting value. So we will need the plugin API function [http://planetspads.free.fr/spads/doc/spadsPluginApiDoc.html#getSpadsConf getSpadsConf].
* perform processing according to our configuration ("words" and "immuneLevel" settings): we need to access our plugin configuration, so we will need the plugin API function [http://planetspads.free.fr/spads/doc/spadsPluginApiDoc.html#getPluginConf-pluginName-caller getPluginConf].
* retrieve the autohost access level of the user: we will use the plugin API function [http://planetspads.free.fr/spads/doc/spadsPluginApiDoc.html#getUserAccessLevel-user getUserAccessLevel]
* send a message to the battle lobby when we kick someone: we will use the plugin API function [http://planetspads.free.fr/spads/doc/spadsPluginApiDoc.html#sayBattle-message sayBattle].
* send a [http://springrts.com/dl/LobbyProtocol/ProtocolDescription.html#KICKFROMBATTLE:client KICKFROMBATTLE] lobby command to kick a user: we will use the plugin API function [http://planetspads.free.fr/spads/doc/spadsPluginApiDoc.html#queueLobbyCommand-lobbyCommand 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:
{|style="margin-top: 10px; margin-left: 40px; background-color: #38474E; border: 1px solid black; border-color: #20292E" |
|
  # This is the handler we set up on SAIDBATTLE lobby command.
  # It is called each time a player says something in the battle lobby.
  sub hLobbySaidBattle {
   
    # $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
    my ($command,$user,$message)=@_;
   
    # First we check it's not a message from SPADS (so we don't kick ourself)
    my $p_spadsConf=getSpadsConf();
    return if($user eq $p_spadsConf->{lobbyLogin});
   
    # Then we check the user isn't a privileged user
    # (autohost access level >= immuneLevel)
    my $p_conf=getPluginConf();
    return if(getUserAccessLevel($user) >= $p_conf->{immuneLevel});
   
    # We put the forbidden words in a array
    my @forbiddenWords=split(/;/,$p_conf->{words});
   
    # We test each forbidden word
    foreach my $forbiddenWord (@forbiddenWords) {
     
      # If the message contains the forbidden word (case insensitive)
      if($message =~ /\b$forbiddenWord\b/i) {
       
        # Then we kick the user from the battle lobby
        sayBattle("Kicking $user from battle (watch your language!)");
        queueLobbyCommand(["KICKFROMBATTLE",$user]);
       
        # We quit the foreach loop (no need to test other forbidden word)
        last;
       
      }
   
    }
 
  }
|}
Once we put all that together, we obtain [http://planetspads.free.fr/spads/plugins/tutorials/ForbiddenWords.pm this fully functional ForbiddenWords] plugin and [http://planetspads.free.fr/spads/plugins/tutorials/ForbiddenWords.conf its associated configuration file].
 
== Testing our plugin ==
 
To test our plugin, we have to put the plugin module in [http://planetspads.free.fr/spads/doc/spadsDoc_All.html#global:pluginsDir SPADS plugins directory], and the associated configuration file in [http://planetspads.free.fr/spads/doc/spadsDoc_All.html#global:etcDir 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.
 
= New-command plugin tutorial (TimePlugin) =
 
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".
 
== Preparing the template ==
 
This time we need to download 4 files to prepare our plugin: [http://planetspads.free.fr/spads/plugins/templates/commented/MyNewCommandPlugin.pm the new-command plugin template], [http://planetspads.free.fr/spads/plugins/templates/commented/MyNewCommandPlugin.conf the associated configuration file example], [http://planetspads.free.fr/spads/plugins/templates/commented/MyNewCommandPluginHelp.dat the help file example] and [http://planetspads.free.fr/spads/plugins/templates/commented/MyNewCommandPluginCmd.conf 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.pm 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":
{|style="margin-top: 10px; margin-left: 40px; background-color: #38474E; border: 1px solid black; border-color: #20292E" |
|
  # Anyone can call our command from anywhere
  [time]
  ::|0:
|}
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:
{|style="margin-top: 10px; margin-left: 40px; background-color: #38474E; border: 1px solid black; border-color: #20292E" |
|
  [time]
  <nowiki>!</nowiki>time - This command just prints current time
|}
== Writing plugin code ==
 
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.pm ) 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 [http://planetspads.free.fr/spads/doc/spadsPluginApiDoc.html#addSpadsCommandHandler-handlers-replace-0 addSpadsCommandHandler]. This call is located in the plugin constructor. We edit it so that it becomes:
{|style="margin-top: 10px; margin-left: 40px; background-color: #38474E; border: 1px solid black; border-color: #20292E" |
|
  [...]
  # We declare our new command and the associated handler
  addSpadsCommandHandler({time => \&hSpadsTime});
  [...]
|}
We must modify the same way the code which removes this SPADS handler using the plugin API function [http://planetspads.free.fr/spads/doc/spadsPluginApiDoc.html#removeSpadsCommandHandler-commands removeSpadsCommandHandler] in the [http://planetspads.free.fr/spads/doc/spadsPluginApiDoc.html#onUnload-self-context onUnload] event-based callback:
{|style="margin-top: 10px; margin-left: 40px; background-color: #38474E; border: 1px solid black; border-color: #20292E" |
|
  [...]
  # We remove our new command handler
  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 [http://planetspads.free.fr/spads/doc/spadsPluginApiDoc.html#answer-message 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:
{|style="margin-top: 10px; margin-left: 40px; background-color: #38474E; border: 1px solid black; border-color: #20292E" |
|
  # This is the handler for our new command
  sub hSpadsTime {
    my ($source,$user,$p_params,$checkOnly)=@_;
   
    # time is a basic command, we have nothing to check in case of callvote
    return 1 if($checkOnly);
   
    my @time = localtime();
    @time = map(sprintf("%02d",$_),@time);
   
    answer("Current local time: $time[2]:$time[1]:$time[0]");
  }
|}
 
Once we put all that together, we obtain [http://planetspads.free.fr/spads/plugins/tutorials/TimePlugin.pm this fully functional TimePlugin] plugin, and its associated [http://planetspads.free.fr/spads/plugins/tutorials/TimePlugin.conf configuration file], [http://planetspads.free.fr/spads/plugins/tutorials/TimePluginCmd.conf command rights configuration file] and [http://planetspads.free.fr/spads/plugins/tutorials/TimePluginHelp.dat command help file].
 
== Testing our plugin ==
 
To test our plugin, we have to put the plugin module and the plugin help file in [http://planetspads.free.fr/spads/doc/spadsDoc_All.html#global:pluginsDir SPADS plugins directory], and the 2 configuration files in [http://planetspads.free.fr/spads/doc/spadsDoc_All.html#global:etcDir 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]]

Latest revision as of 11:08, 15 July 2024

This page is now hosted on GitHub: SPADS plugin development (Python)