Server Spider

Under construction.

Here is an sample class to access the Server Spider data.

Use the following PHP code to display a list of all available servers.

example_list.php

<?php
 
// css style
print('<style type="text/css">
	body {
		color: silver;
		background: black;
	}
 
	/* quake colors */
	.q3-black { color: gray }
	.q3-red { color: red }
	.q3-green { color: lime }
	.q3-yellow { color: yellow }
	.q3-blue { color: blue }
	.q3-cyan { color: cyan }
	.q3-pink { color: fuchsia }
	.q3-white { color: white }
</style>');
 
// init
require('spider.class.php');
 
$spider = new spider();
 
// display time of last cache
printf('<p>Cache date: %s</p>',
	date('r', $spider->cache_time));
 
// display internal structure of cache array
printf('<p>Data structure: <pre>%s</pre></p>',
	htmlentities(print_r(current($spider->cache), true)));
 
// access a specific server
printf('<p>Server BEER.FREEZER is %s</p>',
	isset($spider->cache['193.200.57.36:27960']) ? 'online' : 'offline');
 
// output all servers
foreach ( $spider->cache as $server ) {
	printf('<p>%s %d/%d(%d) <b>%s</b> %dms %s</p>',
		$server['host'],
		$server['clients'], $server['clients_max'], $server['clients_bot'],
		$server['map'], $server['ping'], $server['name']);
}

If you need to query and display a single server.

example_query.php

<?php
 
// init
require('spider.class.php');
 
$spider = new spider();
 
if ( $query = $spider->query('193.200.57.36:27960') ) {
	foreach ( $query['cvars'] as $cvar ) {
		// do not display our private cvars
		if ( !isset($cvar['key']) ) {
			continue;
		}
 
		printf('<strong>%s</strong>: %s<br />',
			htmlentities($cvar['key']), htmlentities($cvar['value']));
	}
 
	// there are some private cvars available: ping, map_time, map_status, score_red, score_blue
	printf('<p>Ping: %dms</p>', $query['cvars']['ping']['value']);
 
	printf('<p>Red score: %d<br />Blue score: %d</p>',
		$query['cvars']['score_red']['value'],
		$query['cvars']['score_blue']['value']);
 
	// player list
	$teams = array('Free', 'Red', 'Blue', 'Spec');
 
	foreach ( $query['players'] as $player ) {
		printf('<p>Name: %s<br />TLD: %s<br />Score: %d<br />Ping: %dms<br />Team: %s<br />Frozen: %s</p>',
			$player['name'],
			$player['tld'],
			$player['score'],
			$player['ping'],
			$teams[ $player['team'] ],
			$player['flags'] & SPIDER_PLAYER_FROZEN ? 'yes' : 'no');
	}
}
else {
	print('<p>Server is offline</p>');
}

spider.class.php

<?php
 
define('SPIDER_VERSION',				1.0);
 
define('SPIDER_PLAYER_FROZEN',			4);
define('SPIDER_PLAYER_RED_FLAG',		8);
define('SPIDER_PLAYER_BLUE_FLAG',		16);
define('SPIDER_PLAYER_NEUTRAL_FLAG',	32);
 
// php5 class
class spider {
	// settings start
	private		$url = 'http://www.excessiveplus.net/api/spider/getservers.csv';
	private		$cache_file = 'spider.db';
	private		$cache_timeout = 60;
	// settings end
 
	public		$cache = array();
	public		$cache_time = 0;
 
	function __construct( $filename = false ) {
		if ( $filename !== false ) {
			$this->cache_file = $filename;
		}
 
		$this->update_cache();
	}
 
	function sort_server( $server1 = NULL, $server2 = NULL ) {
		if ( $server1 === NULL || $server2 === NULL ) {
			return uasort($this->cache, array($this, 'sort_server'));
		}
 
		// sort by players, desc
		$result = -1 * bccomp($server1['clients'] - $server1['clients_bot'], $server2['clients'] - $server2['clients_bot']);
 
		// sort by clients, desc
		if ( !$result ) {
			$result = -1 * bccomp($server1['clients'], $server2['clients']);
		}
 
		// sort by ping, asc
		if ( !$result ) {
			$result = bccomp($server1['ping'], $server2['ping']);
		}
 
		return $result;
	}
 
	function load_cache() {
		if ( file_exists($this->cache_file) ) {
			$this->cache = unserialize(file_get_contents($this->cache_file));
 
			return true;
		}
 
		return false;
	}
 
	function update_cache() {
		if ( !is_writable($this->cache_file) ) {
			trigger_error('Cache file is not writable: '. $this->cache_file, E_USER_ERROR);
		}
 
		// use our cache until it timeout
		$this->cache_time = @filemtime($this->cache_file);
 
		if ( $this->cache_timeout >= time() - $this->cache_time ) {
			$this->load_cache();
 
			return false;
		}
 
		// try to update
		if ( $fp = @fopen($this->url, 'r') ) {
			$version = fgetcsv($fp);
 
			if ( $version[0] == 'spider' ) {
				if ( $version[1] != 100 ) {
					trigger_error('Server spider protocol does not match', E_USER_ERROR);
				}
 
				$header = fgetcsv($fp);
 
				while ( $line = fgetcsv($fp) ) {
					$server = array_combine($header, $line);
					$server['name_plain'] = strip_tags($server['name']);
 
					$this->cache[ $server['host'] ] = $server;
				}
 
				fclose($fp);
 
				$this->sort_server();
				file_put_contents($this->cache_file, serialize($this->cache));
 
				return true;
			}
		}
 
		// service is down, issue a notice and use the cache
		trigger_error('Could not update the server spider cache');
		$this->load_cache();
 
		return false;
	}
 
	// functions to query a single server
	function sort_player( &$player1 = NULL, $player2 = NULL ) {
		if ( $player2 === NULL ) {
			return uasort($player1, array($this, 'sort_player'));
		}
 
		// sort by team, asc
		$result = bccomp($player1['team'], $player2['team']);
 
		// sort by score, desc
		if ( !$result ) {
			$result = -1 * bccomp($player1['score'], $player2['score']);
		}
 
		return $result;
	}
 
	function query( $host, $timeout = 500 ) {
		list($host, $port) = explode(':', $host);
 
		if ( !$port ) {
			$port = 27960;
		}
 
		if ( $fp = @fsockopen('udp://'. $host, $port, $errno, $errstr, $timeout / 1000) ) {
			stream_set_timeout($fp, 0, $timeout * 1000);
 
			fwrite($fp, "\xFF\xFF\xFF\xFFgetstatus\n");
 
			$ping = microtime(true);
 
			if ( !fgets($fp, 20) ) {
				fclose($fp);
 
				return false;
			}
 
			$ping = (microtime(true) - $ping) * 1000;
 
			// non blocking from here on, we've already got the response
			stream_set_blocking($fp, false);
 
			$info = fgets($fp);
			$players = array();
 
			while ( $player = fgetcsv($fp, 1024, ' ', '"') ) {
				$players[] = array(
					'name' => $player[2],
					'score' => $player[0],
					'ping' => $player[1]
				);
			}
 
			fclose($fp);
 
			// parse server info cvars
			if ( preg_match_all('/\\\\([^\\\\]+)\\\\([^\\\\]+)/', $info, $matches, PREG_SET_ORDER) ) {
				foreach ( $matches as $match ) {
					$key = strtolower($match[1]);
 
					$cvars[$key] = array(
						'key' => $match[1],
						'value' => $match[2]
					);
				}
 
				ksort($cvars);
			}
 
			// server ping
			$cvars['ping']['value'] = $ping;
 
			// parse Excessive Plus compressed info
			if ( isset($cvars['info']) && ($matches = explode("\t", $cvars['info']['value'])) ) {
				$i = 0;
 
				// map info
				$cvars['map_time']['value'] = $this->unpack_int($matches[$i++]);
				$cvars['map_status']['value'] = $this->unpack_int($matches[$i++]);
 
				// team info
				if ( $cvars['g_gametype']['value'] >= 3 ) {
					$match = explode(' ', $matches[$i++]);
 
					$cvars['score_red']['value'] = $this->unpack_int($match[0]);
					$cvars['score_blue']['value'] = $this->unpack_int($match[1]);
				}
 
				// player team + flags
				$info_flags = explode(' ', $matches[$i++]);
				@array_pop($info_flags);
 
				foreach ( (array)$info_flags as $id => $info_flag ) {
					$info_flag = $this->unpack_int($info_flag);
 
					$info_team = $info_flag & ~(4 | 8 | 16 | 32);
					$info_flag -= $info_team;
 
					$players[$id]['team'] = $info_team;
					$players[$id]['flags'] = $info_flag;
				}
 
				// player tld
				$info_tlds = explode(' ', $matches[$i++]);
				@array_pop($info_tlds);
 
				foreach ( (array)$info_tlds as $id => $info_tld ) {
					$players[$id]['tld'] = strtolower($info_tld);
				}
 
				// player time
				$info_times = explode(' ', $matches[$i++]);
				@array_pop($info_times);
 
				foreach ( (array)$info_times as $id => $info_time ) {
					$players[$id]['time'] = $this->unpack_int($info_time);
				}
 
				// unset the cvar, it does not contain any "human readable" information
				unset($cvars['info']);
			}
 
			$this->sort_player($players);
 
			return array('cvars' => $cvars, 'players' => $players);
		}
 
		return false;
	}
 
	function unpack_int( $packed_int ) {
		static	$chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$&()*+,-./:<=>?@[]^_{|}\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0b\x0c\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f";
 
		$len = strlen($packed_int) - 1;
		$result = 0;
 
		for ( $i = 0; $i <= $len; $i++ ) {
			$result += strpos($chars, $packed_int[$i]) * pow(62, $len - $i);
		}
 
		return $result;
	}
}