1<?php
2/**
3 * Qstat Plugin: Displays information about Quake 3 server (or compatible
4 * like Openarena).
5 *
6 * Syntax: {{qstat}} - will be replaced with information about the server
7 *
8 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
9 * @author     Mathieu <mathieu@guim.info>
10 */
11
12if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/');
13if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
14require_once(DOKU_PLUGIN.'syntax.php');
15
16/**
17 * All DokuWiki plugins to extend the parser/rendering mechanism
18 * need to inherit from this class
19 */
20class syntax_plugin_dwqstat extends DokuWiki_Syntax_Plugin {
21
22	/**
23	 * Get an associative array with plugin info.
24	 *
25	 * <p>
26	 * The returned array holds the following fields:
27	 * <dl>
28	 * <dt>author</dt><dd>Author of the plugin</dd>
29	 * <dt>email</dt><dd>Email address to contact the author</dd>
30	 * <dt>date</dt><dd>Last modified date of the plugin in
31	 * <tt>YYYY-MM-DD</tt> format</dd>
32	 * <dt>name</dt><dd>Name of the plugin</dd>
33	 * <dt>desc</dt><dd>Short description of the plugin (Text only)</dd>
34	 * <dt>url</dt><dd>Website with more information on the plugin
35	 * (eg. syntax description)</dd>
36	 * </dl>
37	 * @param none
38	 * @return Array Information about this plugin class.
39	 * @public
40	 * @static
41	 */
42	function getInfo(){
43		return array(
44				'author' => 'Mathieu',
45				'email'  => 'mathieu@guim.info',
46				'date'   => rtrim(io_readFile(DOKU_PLUGIN.'dwqstat/VERSION.txt')),
47				'name'   => 'DWQstat Plugin mostly for Openarena. You can use a proxy server if your PHP installation can\'t use \'stream_socket_*\' functions.',
48				'desc'   => 'Provide informations about a Quake3 (or compatible) server',
49				'url'    => 'http://www.guim.info/dokuwiki/dev/qstat',
50			    );
51	}
52
53	/**
54	 * Get the type of syntax this plugin defines.
55	 *
56	 * @param none
57	 * @return String <tt>'substition'</tt> (i.e. 'substitution').
58	 * @public
59	 * @static
60	 */
61	function getType(){
62		return 'substition';
63	}
64
65	/**
66	 * Where to sort in?
67	 *
68	 * @param none
69	 * @return Integer <tt>6</tt>.
70	 * @public
71	 * @static
72	 */
73	function getSort() {
74		return 100;
75	}
76
77
78	/**
79	 * Connect lookup pattern to lexer.
80	 *
81	 * @param $aMode String The desired rendermode.
82	 * @return none
83	 * @public
84	 * @see render()
85	 */
86	function connectTo($mode) {
87		$this->Lexer->addSpecialPattern('{{qstat}}', $mode, 'plugin_dwqstat');
88		$this->Lexer->addSpecialPattern('{{qstat.+?}}', $mode, 'plugin_dwqstat');
89	}
90
91	/**
92	 * Handler to prepare matched data for the rendering process.
93	 *
94	 * <p>
95	 * The <tt>$aState</tt> parameter gives the type of pattern
96	 * which triggered the call to this method:
97	 * </p>
98	 * <dl>
99	 * <dt>DOKU_LEXER_ENTER</dt>
100	 * <dd>a pattern set by <tt>addEntryPattern()</tt></dd>
101	 * <dt>DOKU_LEXER_MATCHED</dt>
102	 * <dd>a pattern set by <tt>addPattern()</tt></dd>
103	 * <dt>DOKU_LEXER_EXIT</dt>
104	 * <dd> a pattern set by <tt>addExitPattern()</tt></dd>
105	 * <dt>DOKU_LEXER_SPECIAL</dt>
106	 * <dd>a pattern set by <tt>addSpecialPattern()</tt></dd>
107	 * <dt>DOKU_LEXER_UNMATCHED</dt>
108	 * <dd>ordinary text encountered within the plugin's syntax mode
109	 * which doesn't match any pattern.</dd>
110	 * </dl>
111	 * @param $aMatch String The text matched by the patterns.
112	 * @param $aState Integer The lexer state for the match.
113	 * @param $aPos Integer The character position of the matched text.
114	 * @param $aHandler Object Reference to the Doku_Handler object.
115	 * @return Integer The current lexer state for the match.
116	 * @public
117	 * @see render()
118	 * @static
119	 */
120	function handle($match, $state, $pos, &$handler){
121		$match = substr($match,7,-2);
122		$match = explode(" ", trim($match));
123		if (count($match) != 2) {
124			return array();
125		}
126		$status = $this->_getStatus($match[0], $match[1]);
127		return ($status === FALSE) ? array() : $status;
128	}
129
130	/**
131	 * Handle the actual output creation.
132	 *
133	 * <p>
134	 * The method checks for the given <tt>$aFormat</tt> and returns
135	 * <tt>FALSE</tt> when a format isn't supported. <tt>$aRenderer</tt>
136	 * contains a reference to the renderer object which is currently
137	 * handling the rendering. The contents of <tt>$aData</tt> is the
138	 * return value of the <tt>handle()</tt> method.
139	 * </p>
140	 * @param $aFormat String The output format to generate.
141	 * @param $aRenderer Object A reference to the renderer object.
142	 * @param $aData Array The data created by the <tt>handle()</tt>
143	 * method.
144	 * @return Boolean <tt>TRUE</tt> if rendered successfully, or
145	 * <tt>FALSE</tt> otherwise.
146	 * @public
147	 * @see handle()
148	 */
149	function render($mode, &$renderer, $data) {
150		$this->setupLocale();
151		$renderer->info['cache'] = false;
152		if($mode == 'xhtml') {
153			if (empty($data)) {
154				//$renderer->doc .= "No informations available.";
155				$renderer->doc .= $this->getLang('no_info');
156			}
157			else {
158				$infotodisplay = array('mapname');
159				if (array_key_exists('defrag_vers', $data)) {
160					//defrag server
161					array_push($infotodisplay, 'df_promode');
162				}
163				else {
164					// baseoa server
165					if (array_key_exists('g_gametype', $data)) {
166						switch($data['g_gametype']) {
167							case 0:
168							case 3:
169								// ffa, tdm
170								array_push($infotodisplay, 'g_gametype', 'fraglimit', 'timelimit');
171								break;
172							case 4:
173								// ctf
174								array_push($infotodisplay, 'g_gametype', 'capturelimit', 'timelimit');
175								break;
176						}
177					}
178				}
179
180				$buf = '<div class="qstat">';
181				// hostname
182				$buf .= "<h3>".$this->_colorize($data['sv_hostname'], 'server')."</h3>";
183				// informations
184				/*
185				$buf .= "<h4>".
186					'<span class="alignleft">Serveur '.(($data['g_needpass'] == 0) ? '<span class="publicserver">public</span>' : '<span class="privateserver">priv&eacute;</span>').'</span>'.
187					'<span class="alignright">'.$this->_renderInformation('protocol', $data['protocol'])."</span></h4>";
188				*/
189				$buf .= '<h4><span class="alignleft">'.
190					(($data['g_needpass'] == 0) ?
191					$this->getLang('public_server') :
192					$this->getLang('private_server')).
193					'</span></h4>';
194				$buf .= '<div class="information';
195					if (array_key_exists('defrag_vers', $data))
196						$buf .= ' defrag" ';
197					else if (array_key_exists('g_gametype', $data) and $data['g_gametype'] == 4) {
198						$buf .= ' ctf" ';
199						unset($infotodisplay[array_search('g_gametype', $infotodisplay)]);
200					}
201					else if (array_key_exists('g_gametype', $data) and $data['g_gametype'] == 3) {
202						$buf .= ' tdm" ';
203						unset($infotodisplay[array_search('g_gametype', $infotodisplay)]);
204					}
205					else if (array_key_exists('g_gametype', $data) and $data['g_gametype'] == 0) {
206						$buf .= ' ffa" ';
207						unset($infotodisplay[array_search('g_gametype', $infotodisplay)]);
208					}
209					else {
210						$buf .= '"';
211					}
212				$buf .= ">";
213				foreach ($infotodisplay as $key) {
214					$buf .= $this->_renderInformation($key, $data[$key]);
215				}
216				// flagbar
217				$buf .= "</div>";
218				$buf .= $this->_flagbar($data);
219				// players
220				$maxclient = $data['sv_maxclients'] - $data['sv_privateClients'];
221				$buf .= "<h4>{$this->getLang('players')} ".count($data['players'])."/{$maxclient}</h4>";
222				$buf .= '<div class="player"><ul>';
223				foreach ($data['players'] as $item) {
224					$buf .= "<li>{$this->_colorize($item['name'])} : {$item['score']} ({$item['ping']})</li>";
225				}
226				$buf .= "</ul></div>";
227				$buf .= '</div>';
228				$renderer->doc .= $buf;
229			}
230			return true;
231		}
232		return false;
233	}
234
235	// display formatted information
236	function _renderInformation($key, $value) {
237		$str = '';
238		switch ($key) {
239			case 'mapname':
240				$str = '<p class="aligncenter">'.$this->getLang('map').': '.$value.'</p>';
241				break;
242			case 'capturelimit':
243				$str = '<p>'.$this->getLang('flags').": $value</p>";
244				break;
245			case 'fraglimit':
246				$str = '<p>'.$this->getLang('frags').": $value</p>";
247				break;
248			case 'timelimit':
249				$str = '<p>'.$this->getLang('time').": $value</p>";
250				break;
251			case 'protocol':
252				if ($value == '68')
253					$str = '0.7.1';
254				elseif ($value == '69')
255					$str = '0.7.6';
256				elseif ($value == '70')
257					$str = '0.7.7';
258				elseif ($value == '71')
259					$str = '0.8.1';
260				break;
261			case 'df_promode':
262				$str = '<p>'.$this->getLang('physic').': '.(($value == '0') ? 'VQ3' : 'CPM').'</p>';
263				break;
264			case 'g_gametype':
265				$str .= '<p>';
266				if ($value == 0)
267					$str = 'Type : FFA';
268				elseif ($value == 3)
269					$str = 'Type : TDM';
270				elseif ($value == 4)
271					$str = 'Type : CTF';
272				$str .= '</p>';
273				break;
274			default:
275				$str = "<p>$key: $value</p>";
276				break;
277		}
278		return $str;
279	}
280
281	// show flag option (on/off) for the server
282	function _flagbar($data) {
283		$flag = array(
284			'g_delagHitscan' => array('desc' => 'unlagged', 'img' => 'lag.png'),
285			'g_delaghitscan' => array('desc' => 'unlagged', 'img' => 'lag.png'),
286			'g_instantgib' => array('desc' => 'instantgib', 'img' => 'ammo_rg.png'),
287			'g_rockets' => array('desc' => 'all rocket', 'img' => 'ammo_rl.png'),
288		);
289		$mod = array(
290			'baseoa' => array('desc' => 'OpenArena', img => 'baseoa.png'),
291			'unfrag-ifs' => array('desc' => 'unfrag-ifs', img => 'unfrag-ifs.png'),
292			'z3r0x' => array('desc' => 'z3r0x', img => 'z3r0x.png'),
293		);
294		$imgpath = DOKU_BASE.'/lib/plugins/qstat_oatforg/images/';
295		$str = '<div class="flagbar">';
296		// mod iteration
297		foreach ($mod as $key => $value) {
298			$keytotest = ((isset($data['game'])) ? 'game' : 'gamename');
299			if ($data[$keytotest] == $key) {
300				$str .= '<img src="'.$imgpath.$value['img'].'" title="'.$value['desc'].'"/>';
301			}
302		}
303		// flag iteration
304		foreach ($flag as $key => $value) {
305			if ($data[$key] == 1) {
306				$str .= '<img src="'.$imgpath.$value['img'].'" title="'.$value['desc'].'"/>';
307			}
308		}
309		$str .= '</div>';
310		return $str;
311	}
312
313	// retrieve status using the appropriate method
314	function _getStatus($host, $port) {
315		if ($this->getConf('use_proxy'))
316			return $this->_proxy($host, $port);
317		else
318			return array_merge($this->_infos($host, $port), $this->_status($host, $port));
319	}
320
321	// use the proxy to retrieve information
322	function _proxy($host, $port) {
323		$proxy = $this->getConf('proxy_url');
324		$str = file_get_contents("$proxy?host=$host&port=$port");
325		$serverstatus = unserialize($str);
326		if (is_string($serverstatus) and $serverstatus == "Bad host or port") {
327			$serverstatus = FALSE;
328		}
329		return $serverstatus;
330	}
331
332	// ask server for "getinfo"
333	function _infos($host, $port) {
334		$serverinfos = array();
335		$packet = "\xFF\xFF\xFF\xFFgetinfo\x00";
336		$sock = @stream_socket_client("udp://$host:$port");
337		if ($sock === FALSE)
338			return FALSE;
339		@stream_set_timeout($sock, 3);
340		$r = @fwrite($sock, $packet, strlen($packet));
341		if ($r === FALSE)
342			return FALSE;
343		$r = @fread($sock, 1500);
344		if ($r === FALSE)
345			return FALSE;
346		// "\0xFF\0xFF\0xFF\0xFFinfoResponse " => 17
347		$str = trim(substr($r, 17));
348		$split = preg_split("/\\\/", $str);
349		$key = null;
350		foreach ($split as $item) {
351			if ($key == null) {
352				$key = $item;
353			}
354			else {
355				$serverinfos[$key] = $item;
356				$key = null;
357			}
358		}
359		return $serverinfos;
360	}
361
362	// ask server for "getstatus"
363	function _status($host, $port) {
364		$serverstatus = array();
365		$packet = "\xFF\xFF\xFF\xFFgetstatus\x00";
366		$sock = @stream_socket_client("udp://$host:$port");
367		if ($sock === FALSE)
368			return FALSE;
369		@stream_set_timeout($sock, 3);
370		$r = @fwrite($sock, $packet, strlen($packet));
371		if ($r === FALSE)
372			return FALSE;
373		$r = @fread($sock, 1500);
374		if ($r === FALSE)
375			return FALSE;
376		// "\0xFF\0xFF\0xFF\0xFFstatusResponse " => 19
377		$str = trim(substr($r, 19));
378
379		$split = preg_split("/\n/", $str);
380		$status = array_shift($split);
381		// player parsing
382		$players = array();
383		foreach ($split as $p) {
384			$s = preg_split("/\s/", $p);
385			$tmp_name = '';
386			for ($i=2; $i<count($s); $i++) {
387				$tmp_name .= trim($s[$i], "\"").' ';
388			}
389			array_push($players, array(
390						'name' => $tmp_name,
391						'ping' => $s[1],
392						'score' => $s[0]));
393		}
394		$serverstatus['players'] = $players;
395		// status parsing
396		$split = preg_split("/\\\/", $status);
397		$key = null;
398		foreach ($split as $item) {
399			if ($key == null) {
400				$key = $item;
401			}
402			else {
403				$serverstatus[$key] = $item;
404				$key = null;
405			}
406		}
407		return $serverstatus;
408	}
409
410	// colorize a player name with Quake style
411	function _colorize($str,$type='player') {
412		$COLOR = array( 0 => 'black',
413				1 => 'red',
414				2 => 'green',
415				3 => 'yellow',
416				4 => 'blue',
417				5 => 'cyan',
418				6 => 'magenta',
419				7 => 'white');
420		$len = strlen($str);
421		$tmp = '';
422		$span = false;
423		for ($i=0; $i<$len; $i++) {
424			if ($str[$i] == '^' && $str[$i+1] == '^') {
425				if ($type == 'server') {
426					if ($i+2 < $len && is_numeric($str[$i+2])) {
427						if ($span) $tmp .= '</span>';
428						$tmp .= '<span style="color: '.$COLOR[($str[$i+2])%8].';">';
429						$span = true;
430						$i+=3;
431					}
432				}
433				else {
434					$tmp .= $str[$i];
435				}
436			}
437			else if ($str[$i] == '^' && $i+1 < $len && is_numeric($str[$i+1])) {
438				if ($span) $tmp .= '</span>';
439				$tmp .= '<span style="color: '.$COLOR[($str[$i+1])%8].';">';
440				$span = true;
441				$i++;
442			}
443			else if ($str[$i] == '^' && $i+1 < $len && is_string($str[$i+1])) {
444				if ($span) $tmp .= '</span>';
445				$ascii = ord($str[$i+1]);
446				#$color = (($ascii > 96) ? $ascii - 22 : $ascii) - 65;
447				$color = (($ascii > 96) ? $ascii - 22 : $ascii) - 66;
448				$tmp .= '<span style="color: '.$COLOR[$color%8].';">';
449				$span = true;
450				$i++;
451			}
452			else {
453				$tmp .= $str[$i];
454			}
455		}
456		if ($span) $tmp .= '</span>';
457		return $tmp;
458	}
459}
460?>
461