diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57872d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/vendor/ diff --git a/README.md b/README.md index 0cd4186..0295195 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,21 @@ # hl-php -PHP library for Half-Life API + +PHP 8 library for Half-Life API with native IPv6 / Yggdrasil support + +## Install + +`composer require yggverse/hl` + +## Usage + +### Xash3d + +#### Master + +``` +$master = new Yggverse\Hl\Xash3d\Master('hl.ygg', 27010); + +var_dump( + $master->getServersIPv6() +); +``` \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..55c3533 --- /dev/null +++ b/composer.json @@ -0,0 +1,17 @@ +{ + "name": "yggverse/hl", + "description": "PHP 8 library for Half-Life API with native IPv6 / Yggdrasil support ", + "type": "library", + "license": "MIT", + "autoload": { + "psr-4": { + "Yggverse\\Hl\\": "src/" + } + }, + "authors": [ + { + "name": "YGGverse" + } + ], + "require": {} +} diff --git a/src/Xash3D/Master.php b/src/Xash3D/Master.php new file mode 100644 index 0000000..c748d53 --- /dev/null +++ b/src/Xash3D/Master.php @@ -0,0 +1,125 @@ +_socket = fsockopen( + "udp://{$host}", + $port + ); + + stream_set_timeout( + $this->_socket, + $timeout + ); + } + + public function __destruct() + { + if ($this->_socket) + { + fclose( + $this->_socket + ); + } + } + + public static function getServersIPv6( + int $limit = 100, + string $region = "\xFF", + string $host = "0.0.0.0:0", + int $port = 0, + string $gamedir = "valve" + ): ?array + { + // Is connected + if (!$this->_socket) + { + return null; + } + + // Filter query + if (!fwrite($this->_socket, "1{$region}{$host}:{$port}\0\gamedir\t{$gamedir}\0")) + { + fclose( + $this->_socket + ); + + return null; + } + + // Skip header + if (!fread($this->_socket, 6)) + { + return null; + } + + // Get servers + $servers = []; + + for ($i = 0; $i < $limit; $i++) + { + // Get host + if (false === $host = fread($this->_socket, 16)) + { + return null; + } + + // Is end of packet + if (true === str_starts_with($host, 0)) + { + break; + } + + // Skip invalid host + if (false === $host = inet_ntop($host)) + { + continue; + } + + // Decode first byte for port + if (false === $byte1 = fread($this->_socket, 1)) + { + return null; + } + + // Decode second byte for port + if (false === $byte2 = fread($this->_socket, 1)) + { + return null; + } + + // Calculate port value + $port = ord($byte1) * 256 + ord($byte2); + + // Validate IPv6 result + if ( + false !== strpos($host, '.') || // filter_var not always works with mixed IPv6 + false === filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) || + false === filter_var($port, FILTER_VALIDATE_INT) + ) + { + continue; + } + + $servers["[{$host}]:{$port}"] = // keep unique + [ + 'host' => $host, + 'port' => $port + ]; + } + + return $servers; + } +} \ No newline at end of file