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é</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