teeworlds-perl

This is a mod for teeworlds-0.5.x. It doesn't support the latest teeworlds version, 0.6.0, and I'm not sure if it ever will. I don't have the inspiration to work on this project right now.

Introduction

Teeworlds is an awesome 2D multiplayer shooter. It features funny cartoon-like graphics, sophisticated movement dynamics, and wide community one can find interesting to participate in. Teeworlds has three standard game types similar to those present in any first-person multiplayer shooter: deathmatch (dm), team deathmatch (tdm) and capture the flag (ctf). There also are plenty of modifications (mods), some of which change the gameplay quite dramatically. This site is devoted to one of such mods.

The situation with modifications in teeworlds world is rather stressed. There is no mod support in the code, so teeworlds design does not provide simple API to create mods. Most of mods has to significantly change the code, and hence every single mod is a standalone full-blown fork. This makes it difficult to combine mods and often the same features are implemented separately by different people in different ways for different mods. The mod described below is not an exception. Fortunately, there was a quite successful attempt to combine several mods into one made by Tom. The result features standard gametypes as well as instagib deathmatch (idm), team deathmatch (itdm), capture the flag (ictf), freeze (ifreeze), freeze capture the flag (ifrzctf) and flagball (ifb). Since this mod had most of my favourite gametypes, I decided to take it as a start and develop it further to implement what I wanted.

The idea for the mod arised when I tried to make the server more configurable. I wanted to make a random maprotation, but couldn't even make a map to appear in rotation twice. While this particular task could easily be solved with a small patch, I thought that it would be a nice idea to have a full-scaled programming language for configuration files. I didn't want to reinvent the bicycle trying to extend the available teeworlds parser or make my own language even if it would be backward-compatible with existing server configurations. Instead, I've chosen Perl, mainly because I wanted to study its internals and figure out how to embed it in programs. Of course, there were a lot of other choices. I've heard that Lua will be used for the same or similar task in the upcoming teeworlds 0.6. Indeed, it seems to be the most suitable language despite that I don't like some of its features: it's small, fast, and easy to embed. Among other choices were Python, Tcl and Scheme. Technically, it doesn't really matter which language to use for this task as long as it is dynamic, has some form of eval function and can call C functions (and if I wouldn't want to have REPL, only the latter requirement would matter). In any case it is a bad idea to often call functions from embedded language because of all the penalties foreign function calls imply, so the speed of the embedded language doesn't really matter. Other requirements, such as memory usage and few dependencies, exclude several major languages, but still plenty of scripting languages are left. The choice between them is more likely a matter of desire. As long as you have a sane API, to implement support for another language is quite easy. Bad thing is that Teeworlds doesn't have one, so I had to simulate it with some hacks.

Anyway, the major points of this mod are:

However, there also are major drawbacks. First, the server administrator is better to know Perl well. If you want something similar to my configuration, you can use examples provided below, but you should understand what you are doing. Second, the server is unlikely to compile under non-POSIX environment, notably in Windows. I haven't tried though. Third, current approach to the definition of command sets for user groups requires writing quite a lot of code. Finally, consider carefully security issues described in chapter REPL.

Downloads

The mod is accessible via the following link:

teeworlds-perl-1.1.0.tar.bz2

You may get the current development version from git with the following command:

git clone git://jini-zh.org/teeworlds-perl

Follow these rules to compile the server. In addition to programs listed there, you will need perl of version not older than 5.10.1 compiled with shared libraries (libperl.so). Note that you need bam-0.2.0, not the latest version of bam. As is with Tom's sources, the client cannot compile in this mod. Issue bam server_release in the final stage of the compilation.

An example configuration is provided in doc/example.pl.

Concepts

REPL

In addition to providing programming language for configuration files, I decided to implement a REPL (read-evaluate-print loop) for rcon. It makes some form of terminal emulation in rcon and gives the administrator complete control over the server (well, the level of control the server API provides). Note that it is easy to extend this control to get access to things unrelated to teeworlds, for example to get a shell on the machine running teeworlds with the rights of the user running teeworlds. It is easy enough to be done manually, right in the rcon command line, so be sure to pick strong root password if you choose to activate it. Of course, there still exists the possibily of root password to be compromised. To minimize possible damage, you may choose between two security measures.

The best way to protect your system is to run the server in chroot jail under unprivileged user. Make a directory, for example /srv/teeworlds/, and put there files you want to be available to the server. In particular, make a maps directory inside /srv/teeworlds and put maps there. Then write a call to init function in the end of your configuration file:

init '/srv/teeworlds', 1000, 1001;

When init function is executed, the server chroots to directory passed as the first parameter, /srv/teeworlds in this example, and changes its group identity and user identity to those specified by the second (1000) and the third (1001) parameters correspondingly. init also copies system files neccessary for teeworlds to work properly. These are /etc/resolv.conf, and libraries /lib/libnss_dns.so.2, /lib/libnss_files.so.2, /lib/libresolv.so.2. Teeworlds uses these files to establish connection to master servers. Since chroot call can be made only by superuser, the server has to be run as root.

If you don't want to bother root every time you want to run the server, you may consider schroot approach.

Groups

Often it is desirable to have several types of users administering the server. For example, there could be administrators with full access to the server commands, and referees who should be able to kick any user by rcon, but should not be able to shutdown the server. With REPL this desire becomes even stronger, because anyone who has access to the REPL, has access to the shell on the server host system and can do a lot of nasty things. In this mod a group concept is introduced to provide control for who can execute which functions.

Available groups are described by a global variable %groups. It is a hash which keys are group names and values are references to hashes with the following keys:

Available functions are described by a global variable %commands. It also is a hash which keys are command names and values are references to hashes with the following keys:

Whenever a user enters a password in order to authenticate in rcon, the server searches for a group with the corresponding password field and assignes its group id to the user. The user can execute only those functions whose group ids match their group id. Two group ids match if their binary and is not zero:

$gid1 & $gid2 != 0

Let's consider a simple example. Suppose that there are two groups:

%groups = (
  admin => {
   gid => 2,
   password => 'admin password'
  },
  user  => {
   gid => 1,
   password => 'user password'
  }
 );

and three functions:

 %commands = (
  kick => {
   gid => 2,
   sub => sub { ... },
  },
  vote => {
   gid => 1 | 2,
   sub => sub { ... },
  },
  complain => {
   gid => 1,
   sub => sub { ... },
  }
 );

In this example kick is executable only by administrator, complain—only by user and vote by both.

Users can belong to several groups at once either if the gid of the group they are logged into matches gids of other groups or if they log into several groups by means of the su command.

There are two special group ids. Group id equal to zero designates an unregistered player. Obviously such players cannot execute rcon commands, but they can call predefined votes, so you may have to take them into account. Group id equal to $root_id which is ~0—the exact opposite of zero—is an id for the root group. Root has direct access to REPL and executes commands in Perl syntax. Note that you have to define the root group in order to turn it on.

Be careful when assigning group numbers. For the simplest case of not-intersecting groups these should be powers of two (or, strictly speaking, a set of orthogonal binary vectors). In general the more permissions the group has, the bigger should be its group id.

Votes

For a command to be votable, it has to have a vgid entry in the %commands hash. Naturally, only users whose gid match vgid have permissions to run the vote. However, vgid is not checked when a predefined vote is called, that is a vote added by addvote to the list of votes the client can call without authorizing in rcon.

Often it is desirable to check command parameters to prevent absurd or undesirable votes. This can be done with entry vcheck which should be a reference to a function that is passed command parameters and dies if it doesn't like them. Here is an example:

%commands = (
 sv_map => {
  gid => 0, # this command cannot be executed
  sub => \&sv_map,
  vgid => 1,
  vcheck => sub { 
   # $_[0] is voting client id
   # $_[1] is voting client group id
   # $_[2] is the command parameters
   die "ctf4 is not allowed\n" if $_[2] =~ /^ *ctf4 *$/
  }
 }
)

Unfortunately, such an approach encourages duplication of the code that parses and checks command parameters. Perhaps later it will be changed.

More

more is a simple text viewer that allows the player to read a big bunch of text in the limited space of rcon. Being fed a pack of strings, more splits them so that they look nice and prints a screenful of text at once until all are done. To proceed to the next screen, user has to send any command except those predefined:

Unfortunately, teeworlds rcon was not supposed to be used as a terminal, so it lacks some features. Terminals use mono-spaced font and there is a way to find out the width and the height of the terminal at runtime. None of these is true for teeworlds, so to control the output two variables, $more_rows and $more_cols, were defined. The former is the number of lines more prints per screen. It seems that rcon height is fixed in the code to be 25. $more_cols is a maximum number of characters to print per line. Its value is hand-picked so that the common line of English text looks fine. Obviously it will fail for a line like "WWWWW..." because "W" characters are rather wide, but for common text it should do. Feel free to complain.

API

Configuration variables

Teeworlds uses one huge struct to hold all configuration variables. Values of these variables are used throughout the code everywhere, so change of the variable value produces immediate effect. Only two types are supported: int and char* (string). In the configuration file variables can be accessed via %config hash. Keys of the hash are variable names and values are references to Perl objects implementing three methods:

  1. get: returns the value of the variable.
  2. set: sets the value of the variable.
  3. help: returns a string describing the variable.

In addition integer variables have the following methods:

  1. min: minimum allowed value.
  2. max: maximum allowed value.

An example:

# Change gametype to ctf
$config{'sv_gametype'}->set('ctf');
# Get the scorelimit
$scorelimit = $config{'sv_scorelimit'}->get();

For convenience, functions with the same names as variables are defined. They check if they are being passed a value and execute set or get method correspondingly. The example can be written as follows:

sv_gametype('ctf');
$scorelimit = sv_scorelimit();

Feel free to redefine these functions in the configuration file.

Following is a list of configuration variables.

Integer variables
Variable Default Minimum Maximum Description
sv_all_respawn_after_score 0 0 1 Respawn unfrozen tees after one of the teams has been completely frozen (1)
sv_anticamper 1 0 1 Anticamper protection (1)
sv_anticamper_range 100 0 500 Camp radius
sv_anticamper_time 10 0 30 Acceptable camp time in seconds
sv_automelt_display 1 0 1 Display melting progress as decreasing armor (1)
sv_automelt_time 30 0 120 Default tee melting time
sv_booster_usetiles 0 0 1 Activate boost tiles (1)
sv_chatloop_interval 0 0 10000000 Send sv_chatloop_message to chat every this number of milliseconds (2)
sv_external_port 0 0 External port to report to master servers
sv_fb_ball_damage_lose 1 0 1 Turns on/off turnover on any damage (1)
sv_fb_ball_deathtile_idle 3 0 60 Number of seconds needed to reset a ball after hitting a death tile (2)
sv_fb_Ball_direct_pass 1 0 1 Turns on/off direct-passing of ball with hook-key (1)
sv_fb_ball_direct_pass_distance 380 32 3200 Maximum direct-passing distance
sv_fb_ball_offset_x 0 0 1000 X offset of the hookable center of the ball with respect to the lower left corner of the image
sv_fb_ball_offset_y 38 0 1000 Y offset of the hookable center of the ball with respect to the lower left corner of the image
sv_fb_ball_radius 40 0 1000 Ball hookable radius
sv_fb_ball_reset_time 30 1 500 Number of seconds needed to reset an unused ball
sv_fb_ball_velocity 207 10 3000 Velocity of the ball after it got shot
sv_fb_goalcamp 1 0 1 Turns on/off goal camp protection (1)
sv_fb_goalcamp_factor 40 10 100 Goal camp protection radius (value/10.0*sv_fb_goalsize)
sv_fb_goalcamp_time 3 1 600 Number of seconds allowed lingering near a goal
sv_fb_goalsize 64 32 500 Radius defining goal
sv_fb_hook_ball 1 0 1 Turns on/off the possibily to capture the ball by hook (1)
sv_fb_laser_momentum 207 -3000 3000 Momentum passed to a flag when it is hit with laser
sv_fb_owngoal 0 0 1 Turns on/off own goals (1)
sv_fb_owngoal_kick 0 0 1 Number of own goals allowed before kicking the player (2)
sv_fb_owngoal_penalty 30 0 999 Negative score for own goal player
sv_fb_owngoal_warn 1 0 1 Turns on/off warning when player with ball is near own goal (1)
sv_fb_score_player_ball 5 0 100 Player score on ball-goal
sv_fb_score_player_stand 1 0 100 Player score for taking ball from stand
sv_fb_score_player_tee 10 0 100 Player-score on tee-goal
sv_fb_score_team_ball 50 1 100 Team-score on ball-goal
sv_fb_score_team_tee 100 1 100 Team-score on tee-goal
sv_freeze_usetiles 0 0 1 Activate freeze tiles (1)
sv_frozen_tags 1 0 1 Insert [F] in front of frozen tee's name (1)
sv_grenade_reload 500 0 10000 Grenade launcher reload time in milliseconds
sv_grenade_selfdamage 1 0 1 Grenades damage the shooter (1)
sv_grenade_split 0 0 10 Number of grenades to fire per grenade launcher shot. Set to 1 to get inifinite normal grenades (2)
sv_high_bandwidth 0 0 1 Use high bandwidth mode. Doubles the bandwidth required for the server. LAN use only (1)
sv_inactivity_kick 120 0 10000 Kick players not active for that number of seconds (2)
sv_inactivity_kick_spect 0 0 1 Force inactive players to spectate rather than kick (1)
sv_join_as_frozen 0 0 1 Freeze newcomers (1)
sv_killingspree 1 0 1 Activate killing spree messages (1)
sv_killingspree_award 0 0 1 Provide awards for killing sprees (1)
sv_killingspree_award_lasers 3 0 1 For awarded tees, use this instead of sv_lasers
sv_killingspree_award_lasers_split 1 0 10 For awarded tees, use this instead of sv_lasers_split
sv_killingspree_award_reload 75 1 100 For awarded tees weapon reload times become this number of percents of default
sv_killingspree_kills 5 1 1000 Number of kills per killing spree
sv_killingspree_tag 1 0 1 Insert [SPREE] in front of killingspreeing tee's name (1)
sv_laser_gothroughfrozens 0 0 1 Let laser penetrate frozen tees (1)
sv_laser_gothroughplayers 0 0 1 Let laser penetrate players (1)
sv_laserjumps 0 0 1 Activate laser jumps (1)
sv_laser_reload 800 0 10000 Laser reload time in milliseconds
sv_lasers 1 1 10 Number of laser beams per laser rifle shot
sv_lasers_split 1 0 10 Angle between laser beams if sv_lasers is more than 1. Measured in `pi / 100`
sv_map_reload 0 0 1 Reload the current map (1)
sv_maxclients 8 1 16 Maximum number of clients that are allowed on a server
sv_melt_health 10 1 10 Health of melted tee
sv_melt_range 100 0 1000 Tee heat radius
sv_melt_respawn 1 0 1 Respawn melted tees (1)
sv_melt_tiles_degeneration 1 0 10 Amount of damage per second a tee standing on ice tile receives
sv_melt_tiles_noshoot 0 0 1 Allow shooting on hot tiles (1)
sv_melt_time 1 0 5 Melting time in seconds for a tee being thawed
sv_melt_usetiles 0 0 1 Activate melting tiles (1)
sv_pause_warmup 0 0 Number of seconds to do warmup before unpausing the game
sv_port 8303 0 Port to use for the server
sv_powerups 1 0 1 Allow powerups like ninja (1)
sv_register 1 0 1 Register server with master server for public listing (1)
sv_reserved_slots 0 0 16 Keep that many slots reserved to whoever knows the password
sv_rounds_per_map 1 1 100 Number of rounds on each map before rotating (3)
sv_scorelimit 20 0 Score limit (2)
sv_spamprotection 1 0 1 Spam protection (1)
sv_spawnprotect_time 1000 0 10000 Spawnprotection time in milliseconds
sv_spectator_slots 0 0 16 Number of slots to reserve for spectators
sv_teambalance 0 0 1 Activate team auto-balancing
sv_teambalance_time 1 0 1000 How many minutes to wait before autobalancing teams
sv_teamdamage 0 0 1 Team damage (1)
sv_teleport_usetiles 0 0 1 Activate teleport tiles (1)
sv_timelimit 0 0 Time limit in minutes (2)
sv_tournament_mode 0 0 1 Tournament mode. When enabled, players joins the server as spectators (1)
sv_void_frozen 0 0 1 Freeze tees respawned after being killed by death tile (1)
sv_vote_kick 1 0 1 Allow voting to kick players (1)
sv_vote_kick_bantime 5 0 1440 The time to ban a player if kicked by vote. 0 makes it just use kick
sv_warmup 0 0 Number of seconds to do warmup before round starts
String variables
Variable Description
sv_binaddr Address to bind the server to
sv_chatloop_message A message to send to chat every sv_chatloop_interval milliseconds
sv_gametype Game type (4)
sv_map Map to use on the erver
sv_maprotation Maps to rotate between (3)
sv_motd Message of the day to display for the clients
sv_name Server name
sv_reserved_slots_pass Reserved slots password

(1) This is a boolean value. 0 means disabled, 1—enabled.

(2) Zero value disables this option.

(3) on_cyclemap can be used to implement more complex maprotation logic

(4) Available gametypes are: ctf, tdm, dm, ictf, itdm, idm, ifrzctf, ifreeze, ifb.

Functions callable from Perl

Functions providing teeworlds API for the configuration script are described in this section. Description of function signatures uses the following conventions:

The functions:

Functions called from C

Internal

These functions are not supposed to be defined in the configuration script. This documentation is provided for those who would like to extend them.

Public

These are hooks to tune the server reaction to events in game. Unless stated explicitly, these functions are not defined by default.

Implemented Perl functions

Global variables

Interactive functions

An interactive function is a function that may ask the user for additional input while being executed. Since teeworlds is a single-threaded program, it is impossible to implement it in a straightforward way, because the server will hang while the player is talking to the function. Although the multi-threaded approach still can be implemented, I decided to do it in a simpler way. To create an interactive function, define the function that interpretes the user answer and unshift it into the @{$interactive[$RCON_CID]} array. You may use call_interactive for this purpose. You are free to modify $interactive[$RCON_CID][0] during execution. Don't forget to shift the array back when the function returns (with call_interactive you may call $quit->() closure variable). Here is a simple example:

sub ask_me {
 reply 'To be or not to be?';
 call_interactive {
  given ($_[0]) {
   when ('to be') {
    reply 'Ok, to be';
    $quit->();
   };
   when ('not to be') {
    reply 'Are you sure?';
    my $quit_top = $quit;
    call_interactive {
     given ($_[0]) {
      when ('yes') {
       reply 'If you say so';
       $quit->();
       $quit_top->();
      };
      when ('no') {
       reply 'So, to be or not to be?';
       $quit->();
      };
      default {
       reply 'What?';
      }
     }
    }
   };
   default {
    reply 'What??';
   }
  }
 };
 return undef;
}

The $quit variable in the example contains the reference to the code that clears the function out of the @{interactive[$RCON_CID]}.

The concept of interactive functions is not well-thought yet. If you really need such function, perhaps you better read the sources to understand what kind of magic is going on there. Currently only more function uses call_interactive, maybe later if I need other functions, I'll make a saner interface.

Events

Configuration script may customize server behaviour in case of different events. For example, it may define actions that should be done when the game is over or, in case of player leave, release the memory that is no longer needed. That is achieved by means of hooks to server events. Hooks are subroutines stored in %hooks hash. They are called when the corresponding event happens with some parameters describing the event.

In order to ease configuration and let modules to use hooks without clashing with other modules, the %hooks hash stores subroutines in the following way: each entry in %hooks is a hash, elements of which are subroutines and keys are some names assigned to these subroutines, for example

$hooks{'leave'}{'log'} = sub {
  printf LOG '%s has left the game', client_name($_[0]) 
};

defines a function named log that will print a string to the LOG stream when someone leaves the game. When an event happen, all subroutines in the hash stored under the corresponding event name are executed (in this case these are values %{$hooks{'leave'}}). Hence modules can define their functions under module-specific names and avoid the clash with other modules.

Defined events are:

Modules

In order to simplify configuration for several servers, most useful features are provided in form of modules or packages in Perl terminology. To include a module in your script, just use it, for example:

use Teeworlds::Common;

The @INC array that holds paths used by Perl to find module files is modified to include ~/.teeworlds and src/config directories. If you want to run the server from the place other than teeworlds repository directory, either copy src/config/Teeworlds to ~/.teeworlds or modify @INC appropriately.

Common

Teeworlds::Common contains a set of the most common features: rcon commands required to effectively control the server and a few useful functions. It is expected to be included in every not-very-unusual configuration script, so it fills the main namespace with its symbols. Other modules may depend on it.

Teeworlds::Common provides the following functions:

Teeworlds::Common provides the following groups: root, admin, and user, with gids $g_root, $g_admin, $g_user. By default no password is defined, so groups are inaccessible.

Teeworlds::Common provides the following commands:

Captains

Teeworlds::Captains provides support for the captain games. The captain game starts with two players being elected as captains. They join opposing teams and start choose other players in turns. The player chosen joins the team of the corresponding captain. When both teams are built, the actual fight starts as usual.

To start the game, two captains must be elected by means of elect command. By default it can only be voted, so the majority of players have to want to play the captain game and agree on who they want to be their captain. When both captains are elected, everyone is forced to spectate and captains are joined opposing teams. At this stage captains may choose their teammates by means of the choose command or pass their turn to the other team via the skip command. Red team chooses first. By default team balance is not set, so it is possible to play unbalanced games. When team building is done, both captains must invoke ready command and the game starts after a warmup. While playing, the game can be paused via stop command and unpaused when both captains invoke ready command. When someone leaves, the game pauses and it must be unpaused in the same way. If both captains leave, the game is dropped.

To stop the captain game, vote for the drop_game command. The captain may be de-elected via drop_captain command.

Teeworlds::Captains uses a lot of features from Teeworlds::Common and expects it to be included first.

Teeworlds::Captains provides the group captain with gid $g_captain.

Teeworlds::Captains provides the following commands:

Random maps

Teeworlds::Random_maps sets random maprotation on the server. To define the available maps, use %maps hash with keys being map names and values being hashes with keys probability setting the probability for a map to appear. The probabilities don't have to be normed to one; when all maps have been defined, call normalize_maps function to compute real probabilities. They will be stored under the p fields in values of %maps hash. Here is a short usage example:

%maps = (
  ctf2 => { probability => 0.5 },
  ctf4 => { probability => 0.5 },
  ctf5 => { probability => 0.3 },
);

normalize_maps;

After the call to normalize_maps $maps{'ctf2'}{'p'} and $maps{'ctf4'}{'p'} are equal to 0.38, and $maps{'ctf5'}{'p'} is equal to 0.23.

Teeworlds::Random_maps provides the following functions:

The next_map function is also available as a command.