Architecture

Overview

The piqueserver codebase is made up out of two main packages: pyspades and piqueserver. The piqueserver module used to be named feature_server. The original developers wanted to make the pyspades a generic AoS protocol and server implementation which the feature_server module then subclassed and specialized.

However, the unclear division and two locations quickly lead to a large mess, and it is currently not possible to know for certain which module exactly contains certain functionality. In general, this is the rule of thumb:

  • pyspades: Anything that involves sending and receiving of packets and acting on those, keeping game state.
  • piqueserver: Anything player-facing, for example commands, configuration, etc.

Note however the numerous exceptions to this. For example, parts of the command logic are in pyspades, while a lot of server validation logic is in piqueserver

Classes

There are currently two classes which contain the bulk of the logic. Connection, and Protocol. The Connection class does not actually represent a connection, it represents a player connected to the server and is contained in the player.py file in the relevant module. The Protocol object is similar, but represents the server and is in the server.py file.

Many classes and modules have descriptions you can view in this documentations or in the files themselves.

Extension Scripts

Piqueserver supports extension scripts aka “scripts” that modify it’s behaviour. The mechanism used to implement these is pretty ugly.

Each script defines an apply_script(protocol, connection, config) function. This function is called on script initialization with the protocol class, connection class and the config dict. Scripts are intended to then dynamically subclass the protocol and connection classes and return those, overriding methods where needed:

def apply_script(protocol, connection, config):
    class ScriptNameProtocol(protocol):
        def my_overridden_function(self, arg):
            ...
            return protocol.my_overridden_function(self, arg)
        ...

    class ScriptNameConnection(connection):
        ...

    return ScriptNameProtocol, ScriptNameConnection

The application of these scripts is performed in a loop, apply_script always being called with the classes returned by the last invocation. This way, the final class created inherits from all extension classes.

This is a terrible idea for a number of reasons, but just the way things work currently:

  • Extensions must meddle with the internals of piqueserver. This means that scripts are likely to break whenever any internals are changed, making substatial changes harder.
  • It is impossible to load or unload scripts after starting the server
  • There is no clear and defined API for scripts. This makes writing scripts unecessarily difficult and also increases breakage.
  • Debugging this is very hard

Game mode scripts are identical to regular extension scripts in functionality. However, they are required to define an attribute named game_mode on the Protocol class that describes the base game mode, CTF_MODE or TC_MODE. This is required because at a protocol level, AoS currently only supports those two game modes. Any other game modes must be emulated using the functionality provided by these two game modes.