Scripting Tutorial for Armagetron

Written by dukevin (dukevinjduke@gmail.com) suggestions, comments, and questions welcome

How it works

  • The server constantly writes to a file called ladderlog.txt about what is going in the server at that moment.
  • For example, the server would write if someone died, committed suicide, talked, a zone was spawned, or someone entered a zone. The list goes on.
  • When someone commits suicide, the server would write: "DEATH_SUICIDE player_1".
  • With scripting, we can look at the events constantly happening in ladderlog and act upon them with admin commands.
  • For example, when someone types "/jump end", a ladderlog event would be written like this: "INVALID_COMMAND /jump player_1 1.1.1.1 15 end" and we can choose to teleport player_1 based on this.
  • Another example is with map rotators. When the round ends and prepares to load the next round (ladderlog event ROUND_COMMENCING), we can change the MAP_FILE.
  • An incomplete list of ladderlog events can be found here
  • A list of admin commands can be found here
  • You should set LADDERLOG_WRITE_ALL 1 in server_info.cfg so all events are logged.
  • Programming

  • This tutorial will focus on PHP as the main scripting language. However, other languages can be used such as python or shell
  • Good programming comes with practice and lots of work. This tutorial will not teach all aspects of programming, just basics to get you started
  • At the top of the PHP file there should be a shebang: #!/usr/bin/php, on the line following, a php open tag: <?php and at the bottom of the page, a php close tag: ?>
  • Your file now should look like this:
  • #!/usr/bin/php
    <?php
    
    ?>
    
  • All your programming should be inside the php tags.
  • Your program needs to be in an endless loop so that the script does not end. We will use a while loop for this:
  • while(true)
    {
    
    }
    
  • This will create a loop inside the brackets that the script cannot escape from because the while is always true. You can also use 1 instead for true like: while(1)
  • We also need to tell the script to constantly read from ladderlog.txt. Put this in the while loop: $line = rtrim(fgets(STDIN, 1024))
  • All lines end with a semicolon ;, think of it as a period in a sentence. Things that don't end in a semicolon are statements such as if, for, while. So put a semicolon after the line you just typed.
  • Your script should now look like this:
  • #!/usr/bin/php
    <?php
    while(true)
    {
        $line = rtrim(fgets(STDIN, 1024));
       
    }
    ?>
    
  • What this does is put the last line of ladderlog and store it in the variable called $line.
  • More on variables: A variable stores data and can change. In PHP, all variables start with $. So a variable that contains the name of a player could look like: $player
  • To assign data to a variable, use the assignment operator, the = sign. This would assign "player_1" to $player: $player = "player_1";
  • In this example, $line would contain the last event written in ladderlog, so it could contain "PLAYER_KILLED player_1"
  • Writing a script

  • This tutorial will show you how to make a script that spawns a zone when players type '/zone'
  • First, we need to see when someone types /zone. The ladderlog event that happens when someone types /zone is "INVALID_COMMAND"
  • Since $line contains a live feed from ladderlog, we will check to see if $line has "INVALID_COMMAND" in it.
  • We will use preg_match for this but there are other ways to check. preg_match takes 2 parameters, what you are searching, and where to search and will return true if it finds it or false if not.
  • Since we are searching for INVALID_COMMAND and searching in $line, type: if(preg_match( "/^INVALID_COMMAND/", $line))
  • So whenever someone types a command such as /zone /jump /anything, this statement will be true and the code under it will execute.
  • Put curly braces under the preg_match. All code in the { will execute when the if statement is true. Your code should now look like this:
  • #!/usr/bin/php
    <?php
    while(true)
    {
        $line = rtrim(fgets(STDIN, 1024));
    	
    	if(preg_match( "/^INVALID_COMMAND/", $line))
    	{
    	
    	}
    }
    ?>
    
  • But this if statement will become true whenever someone types /something and we only want it to execute when someone has typed /zone.
  • The full format of INVALID_COMMAND looks like this: INVALID_COMMAND /zone player_1 1.1.1.1 15 stuff which is
  • INVALID_COMMAND <command> <playername> <ip of player> <access level> <text after the command>
  • We need to check if <command> is /zone and not /anything else, so we will use the php function explode, but you may also use preg_split.
  • explode takes 2 parameters, what to split the command by, and what to split. Since we want to split every space in $line, we will type explode(" ", $line);
  • But simply exploding doesn't do us any good unless we store that data in a variable. I'll use $data but you can use anything. Your code should now look like this:
  • #!/usr/bin/php
    <?php
    while(true)
    {
        $line = rtrim(fgets(STDIN, 1024));
    	
    	if(preg_match( "/^INVALID_COMMAND/", $line))
    	{
    		$data = explode(" ", $line);
    		
    	}
    }
    ?>
    
  • To recap, the ladderlog event for invalid_command is: INVALID_COMMAND <command> <playername> <ip of player> <access level> <text after the command>
  • After exploding, $data is now an array of all the pieces of the INVALID_COMMAND line. The array location containing the <command> will be array location 1, so we use $data[1].
  • We need to see if the player typed /zone and not something else.
  • So check if $data[1] contains /zone:
  • if($data[1] == "/zone")
  • Here == will compare the data stored in the variable $data[1] with "/zone" and return true if they are indentical and false if not.
  • Again, put curly braces under the if line you just wrote so that the script knows that what you want to execute are under the braces.
  • We want to "spawn_zone" so your script should look like this:
  • #!/usr/bin/php
    <?php
    while(true)
    {
        $line = rtrim(fgets(STDIN, 1024));
    	
    	if(preg_match( "/^INVALID_COMMAND/", $line))
    	{
    		$data = explode(" ", $line);
    		if($data[1] == "/zone")
    		{
    			echo "console_message $data[2] spawned a zone!\n";
    			echo "spawn_zone death 100 100 10 0 0 0 false\n";
    		}
    	}
    }
    ?>
    
  • echo will print the spawn_zone line to a file which the game reads and will execute. You must always end your commands with a newline \n.
  • Remember from earlier how $data stores all the parts of $line in an array? Array location 2 stores the name of the person who typed /zone. So we can access that information as $data[2]
  • More features

  • Let's add a few more features to our script, read the code below and try figure out what's going on:
  • #!/usr/bin/php
    <?php
    while(true)
    {
        $line = rtrim(fgets(STDIN, 1024));
    	
    	if(preg_match( "/^INVALID_COMMAND/", $line))
    	{
    		$data = explode(" ", $line);
    		if($data[1] == "/zone")
    		{
    			echo "console_message $data[2] spawned a zone!\n";
    			echo "spawn_zone death 100 100 10 0 0 0 false\n";
    		}
    		if($data[1] == "/teleport")
    		{
    			echo "console_message $data[2] teleported!\n";
    			echo "teleport_player $data[2] 100 100 1 1\n";
    		}
    		if($data[1] == "/speed")
    		{
    			echo "console_message $data[2] changed the speed!\n";
    			$random = rand(5, 50);
    			echo "cycle_speed $random \n";
    		}
    	}
    }
    ?>
    
  • Now if someone types /teleport, they will be teleported to location 100, 100
  • If someone types /speed, the cycle speed will change randomly between 5 and 50. The rand function gets a random number between 5 and 50 (in our case) and assigns it to the variable $random.
  • If someone types /zone, a deathzone will spawn.
  • Allow the player to choose what kind of zone to spawn and the size.

  • Let's allow them to spawn a winzone, rubberzone, deathzone, or ball.
  • Recall that $data stores everything about the INVALID_COMMAND that the user triggered. If player_1 types /zone death 42 to spawn a dz of size 42, the ladderlog event would look like:
  • INVALID_COMMAND /zone player_1 108.192.44.54 15 death 42
  • So the array indexes of death and 42 will be $data[5] and $data[6] respectively.
  • Now we can check which zone the user wants to spawn and spawn that type. Try this on your own, if you have trouble, here is the solution:
  • if($data[1] == "/zone")
    {
    	echo "console_message $data[2] spawned a zone of size $data[6]!\n";
    	if($data[5] == "death")
    	{
    		echo "spawn_zone death 100 100 $data[6] 0 0 0 false\n";
    	}
    	if($data[5] == "win")
    	{
    		echo "spawn_zone win 100 100 $data[6] 0 0 0 false\n";
    	}
    	if($data[5] == "rubber")
    	{
    		echo "spawn_zone rubber 100 100 $data[6] 0 0 0 1 false\n";
    	}
    	if($data[5] == "ball")
    	{
    		echo "spawn_zone ball 100 100 $data[6] 0 0 0 false\n";
    	}
    }
    

    Disable spawning a winzone and prevent spawning misspelled zones

  • Look at the following code for the solution:
  • if($data[1] == "/zone")
    {
    	echo "console_message $data[2] spawned a zone of size $data[6]!\n";
    	if($data[5] == "death")
    	{
    		echo "spawn_zone death 100 100 $data[6] 0 0 0 false\n";
    	}
    	else if($data[5] == "win")
    	{
    		echo "console_message $data[2] is not allowed to spawn a winzone.\n";
    	}
    	else if($data[5] == "rubber")
    	{
    		echo "spawn_zone rubber 100 100 $data[6] 0 0 0 1 false\n";
    	}
    	else if($data[5] == "ball")
    	{
    		echo "spawn_zone ball 100 100 $data[6] 0 0 0 false\n";
    	}
    	else
    	{
    		echo "console_message $data[2] tried to spawn a $data[5] zone but there is no such thing. \n";
    	}
    }
    
  • Adding the else means that it will execute that line if and only if the other if statements above it are also not true.
  • The ending else will execute if none of the above if statements are true, so in this case, if none of the zones chosen to spawn are "death", "win", "rubber", or "ball".
  • Now we have made it so that someone can spawn 4 kinds of zones and specify the size of the zone.
  • But what if someone types "/zone death" and leaves the size field blank? The zone will not spawn correctly, so we should set a default size for when the user doesn't specify the size of the zone.
  • Read about the php function empty()
  • Add this code below the first opening brace { under "if($data[1] == "zone")":
  • if(empty($data[6]))
    {
    	$data[6] = 10;
    }
    
  • This will set the default size of the zone to 10 if the user does not specify the size of the zone.
  • Let's also specify a default zone type if someone types /zone without a type. Let's make the default a deathzone.
  • Your entire php file should now look like this:
  • #!/usr/bin/php
    <?php
    while(true)
    {
        $line = rtrim(fgets(STDIN, 1024));
    	if(preg_match( "/^INVALID_COMMAND/", $line))
    	{
    		$data = explode(" ", $line);
    		if($data[1] == "/zone")
    		{
    			if(empty($data[6]))
    			{
    				$data[6] = 10;
    				$data[5] = "death";
    			}
    			echo "console_message $data[2] spawned a zone of size $data[6]!\n";
    			if($data[5] == "death")
    			{
    				echo "spawn_zone death 100 100 $data[6] 0 0 0 false\n";
    			}
    			else if($data[5] == "win")
    			{
    				echo "console_message $data[2] is not allowed to spawn a winzone.\n";
    			}
    			else if($data[5] == "rubber")
    			{
    				echo "spawn_zone rubber 100 100 $data[6] 0 0 0 1 false\n";
    			}
    			else if($data[5] == "ball")
    			{
    				echo "spawn_zone ball 100 100 $data[6] 0 0 0 false\n";
    			}
    			else
    			{
    				echo "console_message $data[2] tried to spawn a $data[5] zone but there is no such thing. \n";
    			}
    		}
    		if($data[1] == "/teleport")
    		{
    			echo "console_message $data[2] teleported!\n";
    			echo "teleport_player $data[2] 100 100 1 1\n";
    		}
    		if($data[1] == "/speed")
    		{
    			echo "console_message $data[2] changed the speed!\n";
    			$random = rand(5, 50);
    			echo "cycle_speed $random \n";
    		}
    	}
    }
    ?>
    

    Adding a feature that displays a random message every round

  • The ladderlog event for when the next round loads is ROUND_COMMENCING so we should check ladderlog for this event.
  • If you remember, we used preg_match to do this. Try to write this section on your own below the closing brace } for the other preg_match
  • You should have written: if(preg_match( "/^ROUND_COMMENCING/", $line))
  • If you remember how we used the array, $data to store all the parts of INVALID_COMMAND, we use another array to store a list of all the random messages you want to use. we will create an array called $messages that will store a list of 5 messages:
  • $messages = array("Hello World", "Once upon a time...", "Long before...", "This server is brought to you by", "The fox jumped over the light cycle");
  • If we do this right, every round the server should pick one of those messages and display it on the screen.
  • When counting array indexes, the first element is 0, not 1. So if we wanted to display "Hello World", type "console_message $messages[0]\n";.
  • If you remember how to get a random number, we used rand(). Since we want to pick a number from 0 to 4 since there are 5 messages, we can get a random number like $random = rand(0, 4);
  • And to display the random message, type like this: echo "console_message $messages[$random]\n";
  • Your round_commencing should look like this:
  • if(preg_match( "/^ROUND_COMMENCING/", $line))
    {
    	$messages = array("Hello World", "Once upon a time...", "Long before...", "This server is brought to you by", "The fox jumped over the light cycle");
    	$random = rand(0, 4);
    	echo "console_message $messages[$random]\n";
    }
    
  • If we wanted to add another message, we would have to add it to the end of the $messages array and add another number to rand like rand(0,5). If you want to skip this step, read the documentation for the php function count(). count will give you how many elements you have in your array.
  • If messages looked like this: $messages = array("Hello World", "Once upon a time...", "Long before...", "This server is brought to you by", "The fox jumped over the light cycle"); and you did count($messages), the return value would be 5 because there are 5 messages inside the array.
  • So now we can do this:
  • if(preg_match( "/^ROUND_COMMENCING/", $line))
    {
    	$messages = array("Hello World", "Once upon a time...", "Long before...", "This server is brought to you by", "The fox jumped over the light cycle");
    	$random = rand(0, count($messages));
    	echo "console_message $messages[$random]\n";
    }
    
  • Now we will no longer have to manually count how many messages are in the array.
  • But remember, array indexes start from 0, not 1! count will give you 5, even though the highest array index is 4. We will have to subtract 1 from the count.
  • rand should now look like: $random = rand(0, count($messages)-1);
  • Map rotators use the same method we are using but instead of messages, they have map file names and echo map_file instead of console_message.
  • Our script is now finished. It allows players to spawn zones of their choice and size, and display a random message at the beginning of the round.
  • The finished script

  • You can copy and paste this into your server and it will work:
  • #!/usr/bin/php
    <?php
    while(true)
    {
        $line = rtrim(fgets(STDIN, 1024));
    	if(preg_match( "/^INVALID_COMMAND/", $line))
    	{
    		$data = explode(" ", $line);
    		if($data[1] == "/zone")
    		{
    			if(empty($data[6]))
    			{
    				$data[6] = 10;
    				$data[5] = "death";
    			}
    			echo "console_message $data[2] spawned a zone of size $data[6]!\n";
    			if($data[5] == "death")
    			{
    				echo "spawn_zone death 100 100 $data[6] 0 0 0 false\n";
    			}
    			else if($data[5] == "win")
    			{
    				echo "console_message $data[2] is not allowed to spawn a winzone.\n";
    			}
    			else if($data[5] == "rubber")
    			{
    				echo "spawn_zone rubber 100 100 $data[6] 0 0 0 1 false\n";
    			}
    			else if($data[5] == "ball")
    			{
    				echo "spawn_zone ball 100 100 $data[6] 0 0 0 false\n";
    			}
    			else
    			{
    				echo "console_message $data[2] tried to spawn a $data[5] zone but there is no such thing. \n";
    			}
    		}
    		if($data[1] == "/teleport")
    		{
    			echo "console_message $data[2] teleported!\n";
    			echo "teleport_player $data[2] 100 100 1 1\n";
    		}
    		if($data[1] == "/speed")
    		{
    			echo "console_message $data[2] changed the speed!\n";
    			$random = rand(5, 50);
    			echo "cycle_speed $random \n";
    		}
    	}
    	if(preg_match( "/^ROUND_COMMENCING/", $line))
    	{
    		$messages = array("Hello World", "Once upon a time...", "Long before...", "This server is brought to you by", "The fox jumped over the light cycle");
    		$random = rand(0, count($messages));
    		echo "console_message $messages[$random]\n";
    	}
    }
    ?>