1. <?php
  2. /*
  3. LiveStats Library
  4. version: 2.0
  5. query engines: Goldsource, Source, SA:MP
  6. protocol: 47, 48
  7. author: nr913
  8. visit www.freakz.ro
  9. */
  10. define('HL_PACKET', -1);
  11. define('HL_PACKET_SPLITTED', -2);
  12. define('SAMP_PACKET', 1347240275);
  13. class Player {
  14. var $ID;
  15. var $Name;
  16. var $Score;
  17. var $TimePlayed;
  18. var $Ping;
  19. }
  20. class Server {
  21. var $Type = 0;
  22. var $ProtocolVersion;
  23. var $Address;
  24. var $Hostname;
  25. var $Map;
  26. var $Directory;
  27. var $Description;
  28. var $AppID;
  29. var $MaxPlayers;
  30. var $PlayerCount;
  31. var $BotCount;
  32. var $Dedicated;
  33. var $OS;
  34. var $PasswordProtected;
  35. var $Secured;
  36. var $GameMode;
  37. var $WitnessCount;
  38. var $WitnessTime;
  39. var $GameVersion;
  40. var $Players = array();
  41. var $Rules = array();
  42. }
  43. class Time {
  44. var $Hours;
  45. var $Minutes;
  46. var $Seconds;
  47. function __construct($seconds) {
  48. $this->Hours = floor($seconds / 3600);
  49. $this->Minutes = floor($seconds / 60) % 60;
  50. $this->Seconds = floor($seconds) % 60;
  51. }
  52. function __toString() {
  53. $hours = ($this->Hours < 10 ? '0' . $this->Hours : $this->Hours);
  54. $minutes = ($this->Minutes < 10 ? '0' . $this->Minutes : $this->Minutes);
  55. $seconds = ($this->Seconds < 10 ? '0' . $this->Seconds : $this->Seconds);
  56. return "$hours:$minutes:$seconds";
  57. }
  58. }
  59. class LSError extends Exception {
  60. var $ErrorMessage;
  61. var $ErrorCode;
  62. function __construct($errcode, $errmsg) {
  63. $this->ErrorCode = $errcode;
  64. $this->ErrorMessage = $errmsg;
  65. }
  66. }
  67. class Utils {
  68. static function getTime() {
  69. list($u, $s) = explode(' ', microtime());
  70. return ((float)$u + (float)$s);
  71. }
  72. static function getString($packet, &$offset, $length = -1) {
  73. $len = strlen($packet);
  74. $i = 0;
  75. if ($length == -1) {
  76. while ($offset + $i < $len && $packet[$offset + $i] != "\x00")
  77. $i++;
  78. $offset += $i + 1;
  79. return substr($packet, $offset - $i - 1, $i);
  80. } else {
  81. $i = $length;
  82. if ($len - $offset < $length)
  83. $i = $len - $offset;
  84. $offset += $i;
  85. return substr($packet, $offset - $i, $i);
  86. }
  87. }
  88. static function getByte($packet, &$offset) {
  89. if ($offset >= strlen($packet))
  90. return 0;
  91. $char = unpack('cchar', $packet[$offset]);
  92. $offset++;
  93. return $char['char'];
  94. }
  95. static function getShort($packet, &$offset) {
  96. if ($offset + 2 >= strlen($packet))
  97. return 0;
  98. $short = unpack('sshort', substr($packet, $offset, 2));
  99. $offset += 2;
  100. return $short['short'];
  101. }
  102. static function getInt($packet, &$offset) {
  103. if ($offset + 4 >= strlen($packet))
  104. return 0;
  105. $int = unpack('iint', substr($packet, $offset, 4));
  106. $offset += 4;
  107. return $int['int'];
  108. }
  109. static function getFloat($packet, &$offset) {
  110. if ($offset + 4 >= strlen($packet))
  111. return ((float)0.0);
  112. $float = unpack('ffloat', substr($packet, $offset, 4));
  113. $offset += 4;
  114. return ((float)$float['float']);
  115. }
  116. }
  117. class LiveStats {
  118. var $IP;
  119. var $Port;
  120. var $Sock;
  121. var $Challenge = "\xFF\xFF\xFF\xFF";
  122. var $HaveInfo = false;
  123. var $HavePlayer = false;
  124. var $HaveRules = false;
  125. var $ResendPlayer = false;
  126. var $ResendRules = false;
  127. var $SendQueue = array();
  128. var $MaxExecutionTime = 2;
  129. var $SplittedPackets = array();
  130. var $SAMPHeader = "SAMP";
  131. var $Server;
  132. public function __construct($hostname, $port = 27015) {
  133. $this->IP = gethostbyname($hostname);
  134. $this->Port = $port;
  135. $errno = 0; $errstr = '';
  136. $this->Sock = @stream_socket_client("udp://{$this->IP}:{$this->Port}", $errno, $errstr, 5);
  137. if ($this->Sock === false)
  138. throw new LSError(1, "Could not connect to specified host [$errno]: $errstr.");
  139. if (stream_set_blocking($this->Sock, false) == false)
  140. throw new LSError(2, "Could not set to non-blocking mode.");
  141. if (stream_set_timeout($this->Sock, 3) == false)
  142. throw new LSError(3, "Could not set timeout to 10 seconds.");
  143. $this->Server = new Server;
  144. $this->Server->Address = "{$this->IP}:{$this->Port}";
  145. $ip = explode('.', $this->IP);
  146. $this->SAMPHeader .= chr($ip[0]) . chr($ip[1]) . chr($ip[2]) . chr($ip[3]) . chr($this->Port & 0xFF) . chr(($this->Port >> 8) & 0xFF);
  147. }
  148. function onSplittedPacketReceived($packet) {
  149. $offset = 4;
  150. $ReqID = Utils::getInt($packet, $offset);
  151. if (!isset($this->SplittedPackets[$ReqID])) {
  152. $this->SplittedPackets[$ReqID] = array(
  153. 'Data' => array(),
  154. 'Type' => -1,
  155. 'Packets' => -1
  156. );
  157. }
  158. $this->SplittedPackets[$ReqID]['Data'][] = $packet;
  159. if (count($this->SplittedPackets[$ReqID]['Data']) == 2) {
  160. $offset = 8;
  161. $FB1 = Utils::getByte($this->SplittedPackets[$ReqID]['Data'][0], $offset);
  162. $offset = 8;
  163. $FB2 = Utils::getByte($this->SplittedPackets[$ReqID]['Data'][1], $offset);
  164. if ($FB1 != $FB2) {
  165. $this->SplittedPackets[$ReqID]['Type'] = 0;
  166. $this->SplittedPackets[$ReqID]['Packets'] = ($FB1<<4)>>4;
  167. } else {
  168. $this->SplittedPackets[$ReqID]['Type'] = 1;
  169. $this->SplittedPackets[$ReqID]['Packets'] = $FB1;
  170. }
  171. }
  172. if (count($this->SplittedPackets[$ReqID]['Data']) == $this->SplittedPackets[$ReqID]['Packets']) {
  173. if ($this->SplittedPackets[$ReqID]['Type'] == 0) {
  174. for ($i = 0; $i < $this->SplittedPackets[$ReqID]['Packets']; $i++) {
  175. $offset = 8;
  176. $PckID = Utils::getByte($this->SplittedPackets[$ReqID]['Data'][$i], $offset)>>4;
  177. if ($i != $PckID) {
  178. $aux = $this->SplittedPackets[$ReqID]['Data'][$PckID];
  179. $this->SplittedPackets[$ReqID]['Data'][$PckID] = $this->SplittedPackets[$ReqID]['Data'][$i];
  180. $this->SplittedPackets[$ReqID]['Data'][$i] = $aux;
  181. $i--;
  182. }
  183. }
  184. $packet = "\xFF\xFF\xFF\xFF" . substr($this->SplittedPackets[$ReqID]['Data'][0], 13);
  185. for ($i = 1; $i < $this->SplittedPackets[$ReqID]['Packets']; $i++)
  186. $packet .= substr($this->SplittedPackets[$ReqID]['Data'][$i], 9);
  187. } else {
  188. for ($i = 0; $i < $this->SplittedPackets[$ReqID]['Packets']; $i++) {
  189. $offset = 9;
  190. $PckID = Utils::getByte($this->SplittedPackets[$ReqID]['Data'][$i], $offset);
  191. if ($i != $PckID) {
  192. $aux = $this->SplittedPackets[$ReqID]['Data'][$PckID];
  193. $this->SplittedPackets[$ReqID]['Data'][$PckID] = $this->SplittedPackets[$ReqID]['Data'][$i];
  194. $this->SplittedPackets[$ReqID]['Data'][$i] = $aux;
  195. $i--;
  196. }
  197. }
  198. $packet = "\xFF\xFF\xFF\xFF" . substr($this->SplittedPackets[$ReqID]['Data'][0], 16);
  199. for ($i = 1; $i < $this->SplittedPackets[$ReqID]['Packets']; $i++)
  200. $packet .= substr($this->SplittedPackets[$ReqID]['Data'][$i], 12);
  201. }
  202. unset($this->SplittedPackets[$ReqID]);
  203. $this->onPacketReceived($packet);
  204. }
  205. }
  206. function onPacketReceived($packet) {
  207. $offset = 4;
  208. switch (Utils::getByte($packet, $offset)) {
  209. case 0x41: // challenge
  210. $this->Challenge = substr($packet, 5);
  211. if ($this->ResendPlayer) {
  212. $this->ResendPlayer = false;
  213. $this->requestPlayer();
  214. }
  215. if ($this->ResendRules) {
  216. $this->ResendRules = false;
  217. $this->requestRules();
  218. }
  219. break;
  220. case 0x44: // player
  221. if ($this->HavePlayer)
  222. break;
  223. $offset = 5;
  224. $numplayers = Utils::getByte($packet, $offset);
  225. while ($numplayers) {
  226. $Player = new Player;
  227. $Player->ID = Utils::getByte($packet, $offset);
  228. $Player->Name = Utils::getString($packet, $offset);
  229. $len = strlen($Player->Name);
  230. $Player->Score = Utils::getInt($packet, $offset);
  231. $Player->TimePlayed = new Time(Utils::getFloat($packet, $offset));
  232. $numplayers--;
  233. array_push($this->Server->Players, $Player);
  234. }
  235. $this->HavePlayer = true;
  236. $this->ResendPlayer = false;
  237. break;
  238. case 0x49: // info, source
  239. if ($this->HaveInfo)
  240. break;
  241. $offset = 5;
  242. $this->Server->ProtocolVersion = Utils::getByte($packet, $offset);
  243. $this->Server->Hostname = Utils::getString($packet, $offset);
  244. $this->Server->Map = Utils::getString($packet, $offset);
  245. $this->Server->Directory = Utils::getString($packet, $offset);
  246. $this->Server->Description = Utils::getString($packet, $offset);
  247. $this->Server->AppID = Utils::getShort($packet, $offset);
  248. $this->Server->PlayerCount = Utils::getByte($packet, $offset);
  249. $this->Server->MaxPlayers = Utils::getByte($packet, $offset);
  250. $this->Server->BotCount = Utils::getByte($packet, $offset);
  251. $this->Server->Dedicated = (Utils::getByte($packet, $offset) == ord('d') ? true : false);
  252. $this->Server->OS = (Utils::getByte($packet, $offset) == 'l' ? 'Linux' : 'Windows');
  253. $this->Server->PasswordProtected = (Utils::getByte($packet, $offset) == 1 ? true : false);
  254. $this->Server->Secured = (Utils::getByte($packet, $offset) == 1 ? true : false);
  255. $TheShipAppIDs = array(2400, 2401, 2402, 2412, 2430, 2406, 2405, 2403);
  256. if (in_array($this->Server->AppID, $TheShipAppIDs)) {
  257. $gm = Utils::getByte($packet, $offset);
  258. $GameModes = array('Hunt', 'Elimination', 'Duel', 'Deathmatch', 'Team VIP', 'Team Elimination');
  259. $this->Server->GameMode = (isset($GameModes[$gm]) ? $GameModes[$gm] : 'Unknown');
  260. $this->Server->WitnessCount = Utils::getByte($packet, $offset);
  261. $this->Server->WitnessTime = Utils::getByte($packet, $offset);
  262. }
  263. $this->Server->GameVersion = Utils::getString($packet, $offset);
  264. $this->HaveInfo = true;
  265. break;
  266. case 0x6D: // info, goldsource
  267. if ($this->HaveInfo)
  268. break;
  269. $offset = 5;
  270. Utils::getString($packet, $offset);
  271. $this->Server->Hostname = Utils::getString($packet, $offset);
  272. $this->Server->Map = Utils::getString($packet, $offset);
  273. $this->Server->Directory = Utils::getString($packet, $offset);
  274. $this->Server->Description = Utils::getString($packet, $offset);
  275. $this->Server->PlayerCount = Utils::getByte($packet, $offset);
  276. $this->Server->MaxPlayers = Utils::getByte($packet, $offset);
  277. $this->Server->ProtocolVersion = Utils::getByte($packet, $offset);
  278. $this->Server->Dedicated = (Utils::getByte($packet, $offset) == ord('d') ? true : false);
  279. $this->Server->OS = (Utils::getByte($packet, $offset) == 'l' ? 'Linux' : 'Windows');
  280. $this->Server->AppID = ($this->Server->OS == 'Linux' ? 4 : 5);
  281. $this->Server->PasswordProtected = (Utils::getByte($packet, $offset) == 1 ? true : false);
  282. $IsMod = Utils::getByte($packet, $offset);
  283. if ($IsMod == 1) {
  284. Utils::getString($packet, $offset);
  285. Utils::getString($packet, $offset);
  286. Utils::getByte($packet, $offset);
  287. Utils::getInt($packet, $offset);
  288. Utils::getInt($packet, $offset);
  289. Utils::getByte($packet, $offset);
  290. Utils::getByte($packet, $offset);
  291. }
  292. $this->Server->Secured = (Utils::getByte($packet, $offset) == 1 ? true : false);
  293. $this->Server->BotCount = Utils::getByte($packet, $offset);
  294. $this->HaveInfo = true;
  295. break;
  296. case 0x45: // rules
  297. if ($this->HaveRules)
  298. break;
  299. $offset = 5;
  300. $rulesnum = Utils::getShort($packet, $offset);
  301. while ($rulesnum) {
  302. $Name = Utils::getString($packet, $offset);
  303. $Value = Utils::getString($packet, $offset);
  304. $rulesnum--;
  305. $this->Server->Rules[$Name] = $Value;
  306. }
  307. $this->HaveRules = true;
  308. $this->ResendRules = false;
  309. break;
  310. default:
  311. break;
  312. }
  313. }
  314. function onSampPacketReceived($packet) {
  315. $offset = 10;
  316. switch (Utils::getByte($packet, $offset)) {
  317. case 0x69: // info
  318. if ($this->HaveInfo)
  319. break;
  320. $this->Server->PasswordProtected = (Utils::getByte($packet, $offset) == 1 ? true : false);
  321. $this->Server->PlayerCount = Utils::getShort($packet, $offset);
  322. $this->Server->MaxPlayers = Utils::getShort($packet, $offset);
  323. $len = Utils::getInt($packet, $offset);
  324. $this->Server->Hostname = Utils::getString($packet, $offset, $len);
  325. $len = Utils::getInt($packet, $offset);
  326. $this->Server->GameMode = Utils::getString($packet, $offset, $len);
  327. $len = Utils::getInt($packet, $offset);
  328. $this->Server->Map = Utils::getString($packet, $offset, $len);
  329. $this->HaveInfo = true;
  330. if ($this->Server->PlayerCount >= 100)
  331. $this->HavePlayer = true;
  332. break;
  333. case 0x72: // rules
  334. if ($this->HaveRules)
  335. break;
  336. $rulesnum = Utils::getShort($packet, $offset);
  337. while ($rulesnum) {
  338. $len = Utils::getByte($packet, $offset);
  339. $Name = Utils::getString($packet, $offset, $len);
  340. $len = Utils::getByte($packet, $offset);
  341. $Value = Utils::getString($packet, $offset, $len);
  342. $rulesnum--;
  343. $this->Server->Rules[$Name] = $Value;
  344. }
  345. $this->HaveRules = true;
  346. break;
  347. case 0x64: // player
  348. if ($this->HavePlayer)
  349. break;
  350. $numplayers = Utils::getShort($packet, $offset);
  351. $lastID = 0;
  352. $plusID = 0;
  353. while ($numplayers) {
  354. $Player = new Player;
  355. $Player->ID = Utils::getByte($packet, $offset) + $plusID;
  356. if ($Player->ID < $lastID) {
  357. $Player->ID += 256;
  358. $plusID += 256;
  359. }
  360. $lastID = $Player->ID;
  361. $len = Utils::getByte($packet, $offset);
  362. $Player->Name = Utils::getString($packet, $offset, $len);
  363. $len = strlen($Player->Name);
  364. $Player->Score = Utils::getInt($packet, $offset);
  365. $Player->Ping = Utils::getInt($packet, $offset);
  366. $numplayers--;
  367. array_push($this->Server->Players, $Player);
  368. }
  369. $this->HavePlayer = true;
  370. break;
  371. default:
  372. break;
  373. }
  374. }
  375. function requestInfo() {
  376. array_push($this->SendQueue, "\xFF\xFF\xFF\xFFTSource Engine Query\x00");
  377. array_push($this->SendQueue, $this->SAMPHeader . "i");
  378. }
  379. function requestPlayer() {
  380. array_push($this->SendQueue, "\xFF\xFF\xFF\xFF\x55" . $this->Challenge);
  381. array_push($this->SendQueue, $this->SAMPHeader . "d");
  382. if ($this->Challenge == "\xFF\xFF\xFF\xFF")
  383. $this->ResendPlayer = true;
  384. }
  385. function requestRules() {
  386. array_push($this->SendQueue, "\xFF\xFF\xFF\xFF\x56" . $this->Challenge);
  387. array_push($this->SendQueue, $this->SAMPHeader . "r");
  388. if ($this->Challenge == "\xFF\xFF\xFF\xFF")
  389. $this->ResendRules = true;
  390. }
  391. function GetServer() {
  392. $start = Utils::getTime();
  393. $this->requestInfo();
  394. $this->requestPlayer();
  395. $this->requestRules();
  396. while (!($this->HaveRules && $this->HaveInfo && $this->HavePlayer)) {
  397. if (Utils::getTime() - $start >= $this->MaxExecutionTime) {
  398. if ($this->HaveInfo && $this->HavePlayer) {
  399. $this->HaveRules = true;
  400. } else {
  401. throw new LSError(4, "Timed out.");
  402. }
  403. }
  404. if (count($this->SendQueue) > 0) {
  405. stream_socket_sendto($this->Sock, array_shift($this->SendQueue));
  406. $meta = stream_get_meta_data($this->Sock);
  407. if ($meta['timed_out'])
  408. throw new LSError(5, "Timed out.");
  409. }
  410. usleep(50000);
  411. $packet = fread($this->Sock, 2048);
  412. $meta = stream_get_meta_data($this->Sock);
  413. if ($meta['timed_out'])
  414. throw new LSError(6, "Timed out.");
  415. if (strlen($packet) > 0) {
  416. $offset = 0;
  417. switch (Utils::getInt($packet, $offset)) {
  418. case HL_PACKET:
  419. if (!in_array($this->Server->Type, array(0, 1)))
  420. break;
  421. $this->Server->Type = 1;
  422. $this->onPacketReceived($packet);
  423. break;
  424. case HL_PACKET_SPLITTED:
  425. if (!in_array($this->Server->Type, array(0, 1)))
  426. break;
  427. $this->Server->Type = 1;
  428. $this->onSplittedPacketReceived($packet);
  429. break;
  430. case SAMP_PACKET:
  431. if (!in_array($this->Server->Type, array(0, 2)))
  432. break;
  433. $this->Server->Type = 2;
  434. $this->onSampPacketReceived($packet);
  435. break;
  436. default:
  437. break;
  438. }
  439. }
  440. }
  441. return $this->Server;
  442. }
  443. }
  444. ?>