1: <?php declare(strict_types=1);
2:
3: /**
4: * Clansuite Server Query
5: *
6: * SPDX-FileCopyrightText: 2003-2025 Jens A. Koch
7: * SPDX-License-Identifier: MIT
8: *
9: * For the full copyright and license information, please view
10: * the LICENSE file that was distributed with this source code.
11: */
12:
13: namespace Clansuite\ServerQuery\ServerProtocols;
14:
15: use function bzdecompress;
16: use function count;
17: use function crc32;
18: use function explode;
19: use function function_exists;
20: use function gmdate;
21: use function implode;
22: use function is_array;
23: use function is_int;
24: use function is_string;
25: use function ksort;
26: use function microtime;
27: use function preg_match;
28: use function preg_replace;
29: use function round;
30: use function str_contains;
31: use function strlen;
32: use function strtolower;
33: use function substr;
34: use function trim;
35: use function unpack;
36: use Clansuite\ServerQuery\CSQuery;
37: use Clansuite\ServerQuery\Util\PacketReader;
38: use Override;
39:
40: /**
41: * Queries a Counter Strike 1.6 server.
42: *
43: * This class works with Counter Strike 1.6 (based on Half-Life engine).
44: */
45: class CounterStrike16 extends CSQuery
46: {
47: /**
48: * Protocol name.
49: */
50: public string $name = 'Counter Strike 1.6';
51:
52: /**
53: * Protocol identifier.
54: */
55: public string $protocol = 'CounterStrike16';
56:
57: /**
58: * Game series.
59: *
60: * @var array<string>
61: */
62: public array $game_series_list = ['Counter-Strike'];
63:
64: /**
65: * List of supported games.
66: *
67: * @var array<string>
68: */
69: public array $supportedGames = ['Counter Strike 1.6'];
70: public string $playerFormat = '/sscore/x2/ftime';
71: public float $response = 0.0;
72:
73: /**
74: * Constructor.
75: */
76: public function __construct(mixed $address, mixed $queryport)
77: {
78: parent::__construct((is_string($address) ? $address : null), (is_int($queryport) ? $queryport : null));
79: }
80:
81: /**
82: * query_server method.
83: */
84: #[Override]
85: public function query_server(bool $getPlayers = true, bool $getRules = true): bool
86: {
87: if ($this->online) {
88: $this->reset();
89: }
90:
91: $starttime = microtime(true);
92:
93: // Use Source engine A2S_INFO query
94: $command = "\xFF\xFF\xFF\xFFTSource Engine Query\x00";
95:
96: // Try up to three times to obtain a valid response, but preserve the
97: // first valid reply to avoid overwriting it with subsequent
98: // '(no response)' fixtures from the mock UDP client.
99: $attempts = 0;
100: $result = false;
101:
102: while ($attempts < 3) {
103: $attempts++;
104: $tmp = $this->sendCommand((string) $this->address, (int) $this->queryport, $command);
105:
106: if ($tmp !== '' && $tmp !== false && $tmp !== '0') {
107: $result = $tmp;
108:
109: break;
110: }
111:
112: if ($result === false) {
113: $result = $tmp;
114: }
115: }
116:
117: if ($result === '' || $result === '0' || $result === false) {
118: $this->errstr = 'No reply received';
119:
120: return false;
121: }
122:
123: $endtime = microtime(true);
124: $diff = round(($endtime - $starttime) * 1000, 0);
125: // response time
126: $this->response = round($diff, 2);
127:
128: // Parse Source engine A2S_INFO response
129: if (strlen($result) < 5) {
130: $this->errstr = 'Response too short';
131:
132: return false;
133: }
134:
135: // Check header (should be 0xFFFFFFFF)
136: $header = $this->unpackFirstValue('N', substr($result, 0, 4));
137:
138: if ($header === null) {
139: $this->errstr = 'Invalid header unpack';
140:
141: return false;
142: }
143:
144: if ($header !== 4294967295) {
145: $this->errstr = 'Invalid header';
146:
147: return false;
148: }
149:
150: // Check response type (should be 'I' for Source engine INFO or 'm' for GoldSource INFO)
151: $responseType = substr($result, 4, 1);
152:
153: $success = false;
154:
155: if ($responseType === 'I') {
156: $success = $this->parseSourceInfo($result);
157: } elseif ($responseType === 'm') {
158: $success = $this->parseGoldSourceInfo($result);
159: } else {
160: $this->errstr = 'Not an INFO response (got: ' . $responseType . ')';
161:
162: return false;
163: }
164:
165: if (!$success) {
166: return false;
167: }
168:
169: // Get players if requested
170: if ($getPlayers && $this->numplayers > 0) {
171: $this->getPlayers();
172: }
173:
174: // Get rules if requested
175: if ($getRules) {
176: $this->getRules();
177: }
178:
179: $this->online = true;
180:
181: return true;
182: }
183:
184: /**
185: * rcon_query_server method.
186: */
187: public function rcon_query_server(string $command, string $rcon_pwd): false|string
188: {
189: $get_challenge = "\xFF\xFF\xFF\xFFchallenge rcon\n";
190:
191: $challengeResponse = $this->sendCommand((string) $this->address, (int) $this->queryport, $get_challenge);
192:
193: if ($challengeResponse === '' || $challengeResponse === '0' || $challengeResponse === false) {
194: $this->debug['Command send ' . $command] = 'No challenge rcon received';
195:
196: return false;
197: }
198:
199: if (preg_match('/challenge rcon (?P<challenge>[0-9]+)/D', $challengeResponse, $matches) !== 1) {
200: $this->debug['Command send ' . $command] = 'No valid challenge rcon received';
201:
202: return false;
203: }
204:
205: $challenge = $matches['challenge'];
206: $commandPayload = "\xFF\xFF\xFF\xFFrcon {$challenge} \"{$rcon_pwd}\" {$command}\n";
207:
208: $result = $this->sendCommand((string) $this->address, (int) $this->queryport, $commandPayload);
209:
210: if ($result === '' || $result === '0' || $result === false) {
211: $this->debug['Command send ' . $command] = 'No rcon reply received';
212:
213: return false;
214: }
215:
216: return $result;
217: }
218:
219: private function getPlayers(): bool
220: {
221: // Use a Source-style flow: send A2S_PLAYER with -1 and inspect reply
222: $playerRequest = "\xFF\xFF\xFF\xFF\x55\xFF\xFF\xFF\xFF";
223:
224: // Send initial player request and get raw response using a persistent socket
225: $initial = $this->sendCommand((string) $this->address, (int) $this->queryport, $playerRequest);
226:
227: if ($initial !== false && strlen($initial) >= 5) {
228: // Inspect header
229: $header = $this->unpackFirstValue('l', substr($initial, 0, 4));
230:
231: if ($header === -1) {
232: $respType = substr($initial, 4, 1);
233:
234: if ($respType === 'D') {
235: // Full player response returned immediately
236: if ($this->parseBinaryPlayers($initial)) {
237: return true;
238: }
239: } elseif ($respType === 'A') {
240: // Challenge returned: extract 4 bytes starting at pos 5 (or 1?)
241: // The challenge is usually the 4 bytes after the response byte
242: if (strlen($initial) >= 9) {
243: $challenge = substr($initial, 5, 4);
244: $playerCommand = "\xFF\xFF\xFF\xFF\x55" . $challenge;
245: $result = $this->sendCommand((string) $this->address, (int) $this->queryport, $playerCommand);
246:
247: if ($result !== false && strlen($result) > 5) {
248: if ($this->parseBinaryPlayers($result)) {
249: return true;
250: }
251: }
252: }
253: }
254: } elseif ($header === -2) {
255: // Multi-packet response: collect and assemble
256: $assembled = $this->collectMultiPacketResponse($initial);
257:
258: if ($assembled !== false && $assembled !== '') {
259: if ($this->parseBinaryPlayers($assembled)) {
260: return true;
261: }
262: }
263: }
264: }
265:
266: // Try GoldSource legacy challenge/players flow as a fallback
267: if ($this->tryGoldSourcePlayers()) {
268: return true;
269: }
270:
271: // Fallback to text-based status command
272: $command = "status\n";
273: $result = $this->sendCommand((string) $this->address, (int) $this->queryport, $command);
274:
275: if ($result !== false && $result !== '') {
276: return $this->parseTextPlayers($result);
277: }
278:
279: $this->players = [];
280:
281: return true;
282: }
283:
284: /**
285: * Basic multi-packet collector. Uses existing _sendCommand to read subsequent parts
286: * and assembles payloads. Supports bzip2 compressed payloads when indicated.
287: */
288: private function collectMultiPacketResponse(string $firstChunk): false|string
289: {
290: // parse header similar to PHP-Source-Query's ReadInternal
291: $reader = new PacketReader($firstChunk);
292:
293: $header = $reader->readInt32();
294:
295: if ($header === null || $header !== -2) {
296: return false;
297: }
298:
299: // RequestID (may include compression flag in high bit)
300: $requestId = $reader->readInt32();
301:
302: if ($requestId === null) {
303: return false;
304: }
305:
306: $isCompressed = ($requestId & 0x80000000) !== 0;
307:
308: // Determine Source vs GoldSource split format by peeking next bytes
309: $parts = [];
310:
311: // Helper to parse a chunk's header and return [count, number, payloadPart, checksum|null]
312: $parseChunk = static function (mixed $chunk): ?array
313: {
314: $r = is_string($chunk) ? new PacketReader($chunk) : null;
315:
316: if ($r === null) {
317: return null;
318: }
319:
320: $h = $r->readInt32();
321:
322: if ($h === null || $h !== -2) {
323: return null;
324: }
325: $rid = $r->readInt32();
326:
327: if ($rid === null) {
328: return null;
329: }
330: $compressed = ($rid & 0x80000000) !== 0;
331:
332: // Attempt Source-style parsing
333: $remaining = strlen($chunk) - $r->pos();
334:
335: if ($remaining >= 2) {
336: $packetCount = $r->readUint8();
337: $packetNumber = $r->readUint8();
338:
339: if ($packetCount !== null && $packetNumber !== null) {
340: $packetNumber = $packetNumber + 1; // Source packet numbers are 1-based in many libs
341: $checksum = null;
342:
343: if ($compressed) {
344: // split size (int32) and checksum(uint32)
345: $r->readInt32(); // ignore split size
346: $checksum = $r->readUint32();
347: } else {
348: // split size int16
349: $r->readUint16();
350: }
351:
352: $payloadPart = $r->rest();
353:
354: return [$packetCount, $packetNumber, $payloadPart, $checksum];
355: }
356: }
357:
358: // Fallback to GoldSource-style parsing
359: $r = is_string($chunk) ? new PacketReader($chunk) : null;
360:
361: if ($r === null) {
362: return null;
363: }
364:
365: $r->readInt32();
366: $r->readInt32();
367: $b = $r->readUint8();
368:
369: if ($b === null) {
370: return null;
371: }
372: $packetCount = $b & 0xF;
373: $packetNumber = $b >> 4;
374: $payloadPart = $r->rest();
375:
376: return [$packetCount, $packetNumber, $payloadPart, null];
377: };
378:
379: // Parse first chunk
380: $firstParsed = $parseChunk($firstChunk);
381:
382: if ($firstParsed === null) {
383: return false;
384: }
385: [$total, $number, $part, $packetChecksum] = $firstParsed;
386: $parts[$number] = $part;
387:
388: // Collect remaining parts
389: $received = count($parts);
390:
391: while ($received < $total) {
392: $chunk = $this->sendCommand((string) $this->address, (int) $this->queryport, '');
393:
394: if ($chunk === false || strlen($chunk) < 8) {
395: break;
396: }
397: $parsed = $parseChunk($chunk);
398:
399: if ($parsed === null) {
400: break;
401: }
402: [$t, $n, $p, $chk] = $parsed;
403: $parts[$n] = $p;
404:
405: if ($chk !== null) {
406: $packetChecksum = $chk;
407: }
408: $received = count($parts);
409: }
410:
411: ksort($parts);
412: $data = implode('', $parts);
413:
414: if ($isCompressed) {
415: if (!function_exists('bzdecompress')) {
416: return false;
417: }
418: $decompressed = bzdecompress($data);
419:
420: if ($decompressed === false) {
421: return false;
422: }
423:
424: if ($packetChecksum !== null && crc32((string) $decompressed) !== $packetChecksum) {
425: return false;
426: }
427:
428: // As upstream does, strip the first 4 bytes from assembled data
429: return substr((string) $decompressed, 4);
430: }
431:
432: return substr($data, 4);
433: }
434:
435: private function tryGoldSourcePlayers(): bool
436: {
437: // Get challenge for GoldSource player query
438: $challengeCommand = "\xFF\xFF\xFF\xFF\x57";
439: $challengeResult = $this->sendCommand((string) $this->address, (int) $this->queryport, $challengeCommand);
440:
441: if ($challengeResult === false || strlen($challengeResult) < 5) {
442: return false;
443: }
444:
445: // Check header
446: $header = $this->unpackFirstValue('N', substr($challengeResult, 0, 4));
447:
448: if ($header === null || $header !== 4294967295) {
449: return false;
450: }
451:
452: // Check response type (should be 0x41 'A')
453: $responseType = substr($challengeResult, 4, 1);
454:
455: if ($responseType !== 'A') {
456: return false;
457: }
458:
459: // Extract challenge number
460: if (strlen($challengeResult) < 9) {
461: return false;
462: }
463: $challenge = substr($challengeResult, 5, 4);
464:
465: // Query players with challenge
466: $playerCommand = "\xFF\xFF\xFF\xFF\x55" . $challenge;
467: $result = $this->sendCommand((string) $this->address, (int) $this->queryport, $playerCommand);
468:
469: if ($result === false || strlen($result) < 5) {
470: return false;
471: }
472:
473: return $this->parseGoldSourcePlayers($result);
474: }
475:
476: private function parseGoldSourcePlayers(string $result): bool
477: {
478: if (strlen($result) < 5) {
479: return false;
480: }
481:
482: // Check header
483: $header = $this->unpackFirstValue('N', substr($result, 0, 4));
484:
485: if ($header === null || $header !== 4294967295) {
486: return false;
487: }
488:
489: // Check response type (should be 0x44 'D')
490: $responseType = substr($result, 4, 1);
491:
492: if ($responseType !== 'D') {
493: return false;
494: }
495:
496: $buf = substr($result, 5);
497: $reader = new PacketReader($buf);
498:
499: $numPlayers = $reader->readUint8();
500:
501: if ($numPlayers === null) {
502: return false;
503: }
504:
505: $players = [];
506:
507: for ($i = 0; $i < $numPlayers; $i++) {
508: // Skip player index byte
509: if ($reader->readUint8() === null) {
510: break;
511: }
512:
513: $name = $reader->readString();
514:
515: if ($name === null) {
516: break;
517: }
518:
519: $score = $reader->readInt32();
520:
521: if ($score === null) {
522: break;
523: }
524:
525: $timeValue = $reader->readFloat();
526:
527: if ($timeValue === null) {
528: break;
529: }
530:
531: $players[] = [
532: 'name' => $name,
533: 'score' => $score,
534: 'time' => gmdate('H:i:s', (int) $timeValue),
535: ];
536: }
537:
538: $this->players = $players;
539:
540: return true;
541: }
542:
543: private function parseBinaryPlayers(string $result): bool
544: {
545: if (strlen($result) < 5) {
546: return false;
547: }
548:
549: // Check header (should be 0xFFFFFFFF)
550: $header = $this->unpackFirstValue('N', substr($result, 0, 4));
551:
552: if ($header === null || $header !== 4294967295) {
553: return false;
554: }
555:
556: // Check response type (should be 'D' for PLAYER)
557: $responseType = substr($result, 4, 1);
558:
559: if ($responseType !== 'D') {
560: return false;
561: }
562:
563: // Parse A2S_PLAYER response
564: $buf = substr($result, 5);
565: $reader = new PacketReader($buf);
566:
567: $numPlayers = $reader->readUint8();
568:
569: if ($numPlayers === null) {
570: return false;
571: }
572:
573: $players = [];
574:
575: for ($i = 0; $i < $numPlayers; $i++) {
576: if ($reader->readUint8() === null) {
577: break;
578: }
579:
580: $name = $reader->readString();
581:
582: if ($name === null) {
583: break;
584: }
585:
586: $scoreRaw = $reader->readUint32();
587:
588: if ($scoreRaw === null) {
589: break;
590: }
591:
592: // Convert unsigned to signed 32-bit
593: if (($scoreRaw & 0x80000000) !== 0) {
594: $scoreRaw -= 0x100000000;
595: }
596:
597: $timeValue = $reader->readFloat();
598:
599: if ($timeValue === null) {
600: break;
601: }
602:
603: // sanitize name: remove non-printable/control characters
604: $name = preg_replace('/[[:^print:]]/', '', $name);
605: $name = trim((string) $name);
606:
607: // Format time: display as H:i:s if >= 3600s, otherwise mm:ss
608: $timeInt = (int) round($timeValue);
609:
610: if ($timeInt >= 3600) {
611: $timeStr = gmdate('H:i:s', $timeInt);
612: } else {
613: $timeStr = gmdate('i:s', $timeInt);
614: }
615:
616: $players[] = [
617: 'name' => $name,
618: 'score' => $scoreRaw,
619: 'time' => $timeStr,
620: ];
621: }
622:
623: $this->players = $players;
624:
625: return true;
626: }
627:
628: private function parseTextPlayers(string $result): bool
629: {
630: // Parse text-based status response
631: $lines = explode("\n", trim($result));
632:
633: $players = [];
634: $inPlayersSection = false;
635:
636: /** @var null|array<string, mixed> $currentPlayer */
637: $currentPlayer = null;
638:
639: foreach ($lines as $line) {
640: $line = trim($line);
641:
642: if ($line === '') {
643: continue;
644: }
645:
646: if ($line === '0') {
647: continue;
648: }
649:
650: // Look for the start of players section
651: if (strtolower($line) === 'players') {
652: $inPlayersSection = true;
653:
654: continue;
655: }
656:
657: if (!$inPlayersSection) {
658: continue;
659: }
660:
661: // Check if this is a player number line (like "2.")
662: if (preg_match('/^(\d+)\.$/', $line, $matches) === 1) {
663: // This is a player number, previous lines should have name and time
664: if ($currentPlayer !== null && isset($currentPlayer['name'], $currentPlayer['time'])) {
665: $players[] = [
666: 'name' => $currentPlayer['name'],
667: 'score' => $currentPlayer['score'] ?? 0,
668: 'time' => $currentPlayer['time'],
669: ];
670: }
671: $currentPlayer = ['index' => (int) $matches[1]];
672:
673: continue;
674: }
675:
676: // Check if this looks like a time format (HHH:MM:SS)
677: if (preg_match('/^\d{1,3}:\d{2}:\d{2}$/', $line) === 1) {
678: if ($currentPlayer !== null) {
679: $currentPlayer['time'] = $line;
680: }
681:
682: continue;
683: }
684:
685: // If it doesn't match the above patterns and we have a current player,
686: // it might be the player name
687: if ($currentPlayer !== null && !isset($currentPlayer['name'])) {
688: $currentPlayer['name'] = $line;
689:
690: continue;
691: }
692:
693: // Fallback: try the original format
694: if (preg_match('/^#(\d+)\s+"([^"]+)"\s+(\d+)\s+([\d:]+)\s+/', $line, $matches) === 1) {
695: $players[] = [
696: 'name' => $matches[2],
697: 'score' => (int) $matches[3],
698: 'time' => $matches[4],
699: ];
700: }
701: }
702:
703: // Don't forget the last player
704: if ($currentPlayer !== null && isset($currentPlayer['name'], $currentPlayer['time'])) {
705: $players[] = [
706: 'name' => $currentPlayer['name'],
707: 'score' => $currentPlayer['score'] ?? 0,
708: 'time' => $currentPlayer['time'],
709: ];
710: }
711:
712: $this->players = $players;
713:
714: return true;
715: }
716:
717: /**
718: * Get rules from the server.
719: */
720: private function getRules(): bool
721: {
722: $command = "\xFF\xFF\xFF\xFFrules\x00\x00";
723:
724: $result = $this->sendCommand((string) $this->address, (int) $this->queryport, $command);
725:
726: if ($result === '' || $result === '0' || $result === false) {
727: return false;
728: }
729:
730: if (strlen($result) < 5) {
731: return false;
732: }
733:
734: // Check header
735: $header = $this->unpackFirstValue('N', substr($result, 0, 4));
736:
737: if ($header === null || $header !== 4294967295) {
738: return false;
739: }
740:
741: // Check indicator (should be 'E' for rules)
742: $indicator = substr($result, 4, 1);
743:
744: if ($indicator !== 'E') {
745: return false;
746: }
747:
748: $data = substr($result, 5);
749: $pos = 0;
750:
751: if (strlen($data) - $pos < 2) {
752: return false;
753: }
754:
755: $numRulesValue = $this->unpackFirstValue('n', substr($data, $pos, 2));
756:
757: if ($numRulesValue === null) {
758: return false;
759: }
760: $pos += 2;
761:
762: $rules = [];
763:
764: for ($i = 0; $i < $numRulesValue; $i++) {
765: if ($pos >= strlen($data)) {
766: break;
767: }
768:
769: $ruleName = $this->readString($data, $pos);
770: $ruleValue = $this->readString($data, $pos);
771: $rules[$ruleName] = $ruleValue;
772: }
773:
774: $this->rules = $rules;
775:
776: return true;
777: }
778:
779: /**
780: * Read a null-terminated string from data.
781: */
782: private function readString(string $data, int &$pos): string
783: {
784: $start = $pos;
785:
786: while ($pos < strlen($data) && $data[$pos] !== "\x00") {
787: $pos++;
788: }
789: $string = substr($data, $start, $pos - $start);
790: $pos++; // Skip null terminator
791:
792: return $string;
793: }
794:
795: /**
796: * Safely unpack binary data and return the first value.
797: */
798: private function unpackFirstValue(string $format, string $data): mixed
799: {
800: $unpacked = unpack($format, $data);
801:
802: if (!is_array($unpacked) || !isset($unpacked[1])) {
803: return null;
804: }
805:
806: return $unpacked[1];
807: }
808:
809: /**
810: * Parse Source engine A2S_INFO response.
811: */
812: private function parseSourceInfo(mixed $result): bool
813: {
814: if (!is_string($result) || strlen($result) < 6) {
815: return false;
816: }
817:
818: $reader = new PacketReader(substr($result, 5));
819:
820: // Read protocol version (ignored, but must consume the byte)
821: if ($reader->readUint8() === null) {
822: return false;
823: }
824:
825: $serverTitle = $reader->readString();
826: $mapName = $reader->readString();
827: $gameDir = $reader->readString();
828: $gameName = $reader->readString();
829:
830: if ($serverTitle === null || $mapName === null || $gameDir === null || $gameName === null) {
831: return false;
832: }
833:
834: $appId = $reader->readUint16();
835:
836: if ($appId === null) {
837: return false;
838: }
839:
840: if ($reader->remaining() < 3) {
841: return false;
842: }
843:
844: $numPlayers = $reader->readUint8();
845: $maxPlayers = $reader->readUint8();
846: $password = $reader->readUint8();
847:
848: $gameVersion = $reader->readString();
849:
850: if ($gameVersion === null) {
851: return false;
852: }
853:
854: $this->servertitle = $serverTitle;
855: $this->mapname = $mapName;
856: $this->gamename = $gameName;
857: $this->steamAppID = $appId;
858: $this->numplayers = $numPlayers;
859: $this->maxplayers = $maxPlayers;
860: $this->password = $password > 0 ? 1 : 0;
861: $this->gameversion = $gameVersion;
862:
863: // Set game type
864: $this->gametype = 'Counter-Strike 1.6';
865:
866: $this->online = true;
867:
868: return true;
869: }
870:
871: /**
872: * Parse GoldSource INFO response.
873: */
874: private function parseGoldSourceInfo(string $result): bool
875: {
876: if (strlen($result) < 5) {
877: return false;
878: }
879:
880: $header = $this->unpackFirstValue('N', substr($result, 0, 4));
881:
882: if ($header === null || $header !== 4294967295) {
883: return false;
884: }
885:
886: $reader = new PacketReader(substr($result, 5));
887:
888: // GoldSource INFO layout is:
889: // address, hostname, map, game_dir, game_descr, num_players, max_players, version, ...
890: $reader->readString(); // address (ignored)
891:
892: $serverTitle = $reader->readString();
893: $mapName = $reader->readString();
894: $gameDir = $reader->readString();
895: $gameName = $reader->readString();
896:
897: if ($serverTitle === null || $mapName === null || $gameDir === null || $gameName === null) {
898: return false;
899: }
900:
901: $currentPlayers = $reader->readUint8();
902: $maxPlayers = $reader->readUint8();
903:
904: if ($currentPlayers === null || $maxPlayers === null) {
905: return false;
906: }
907:
908: $gameVersion = $reader->readString();
909:
910: if ($gameVersion === null) {
911: return false;
912: }
913:
914: $this->servertitle = $serverTitle;
915: $this->mapname = $mapName;
916: $this->gamename = $gameName;
917: $this->numplayers = $currentPlayers;
918: $this->maxplayers = $maxPlayers;
919: $this->gameversion = $gameVersion;
920:
921: // Set game type based on game directory
922: if (str_contains(strtolower($gameDir), 'cstrike')) {
923: $this->gametype = 'Counter-Strike 1.6';
924: } elseif (str_contains(strtolower($gameDir), 'czero')) {
925: $this->gametype = 'Counter-Strike: Condition Zero';
926: } else {
927: $this->gametype = $this->gamename;
928: }
929:
930: return true;
931: }
932: }
933: