PHPackages                             t0xicvybez/gamequery - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. t0xicvybez/gamequery

ActiveLibrary

t0xicvybez/gamequery
====================

A dependency-free PHP library for querying multiplayer game server status (players, map, ping, rules) over UDP/TCP, with concurrent multi-server polling.

v0.5.5(1mo ago)021AGPL-3.0-or-laterPHPPHP &gt;=8.1CI failing

Since Jul 21Pushed 1mo agoCompare

[ Source](https://github.com/t0xicVybez/GameQuery)[ Packagist](https://packagist.org/packages/t0xicvybez/gamequery)[ Docs](https://query.arkenbot.app)[ RSS](/packages/t0xicvybez-gamequery/feed)WikiDiscussions main Synced 2w ago

READMEChangelog (2)Dependencies (2)Versions (3)Used By (0)

GameQuery
=========

[](#gamequery)

[![Packagist Version](https://camo.githubusercontent.com/04bb273cb0de4e1bca77180fd66fd64dca6a22b4a63cf1c2f7ad1cf8c43672c3/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7430786963767962657a2f67616d6571756572793f6c6162656c3d7061636b6167697374)](https://packagist.org/packages/t0xicvybez/gamequery)[![npm version](https://camo.githubusercontent.com/dbd89be9c3774193d3859ed7a7a4f2ea4abeaafcc38c2de0c05d8fe1a84bec05/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f762f407430786963767962657a2f67616d6571756572793f6c6162656c3d6e706d)](https://www.npmjs.com/package/@t0xicvybez/gamequery)[![PHP 8.1+](https://camo.githubusercontent.com/fc1cca7b3a4ab49a852707f7ae3d39f13ab7c8a2de342f7e70f8443fd2d52aec/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7068702d382e312532422d373737626234)](composer.json)[![License: AGPL v3](https://camo.githubusercontent.com/e75361a6644d5c0f8c3efce74ee774f25811f1c1b9a2d7c7d3a1f05a33d790a3/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4147504c2d2d332e302d626c7565)](LICENSE)

**[query.arkenbot.app](https://query.arkenbot.app)** — docs, the full game list, and a live demo you can point at your own server.

A dependency-free game server query library — read player counts, map, hostname, ping, and rules from one or many servers at once. It ships as **two parallel ports that stay in lockstep**: a **PHP** library (`t0xicvybez/gamequery`) and a **Node/TypeScript** library (`@t0xicvybez/gamequery`), with the same protocols, the same API, and the same results.

- **Zero runtime dependencies.** PHP runs on core streams (8.1+); Node runs on built-in `dgram`/`net` (18+). Nothing to `require`, nothing to audit.
- **Concurrent by design.** One event loop drives every server's query at once, so polling 25 servers takes about as long as the single slowest one — not the sum of all of them.
- **23 protocol families / 33 registered keys**, covering A2S/Source, both Minecraft editions, FiveM, Palworld, the GameSpy and id Tech families, voice servers, and more (full table below).
- **Never throws for an unreachable server.** Every query returns a `Result`; offline servers come back with `online = false` and an `error` string.

Built by [@t0xicVybez](https://github.com/t0xicVybez) and used in production by [ArkenBot](https://github.com/t0xicVybez/ArkenBot).

Installation
------------

[](#installation)

**PHP (Composer):**

```
composer require t0xicvybez/gamequery
```

**PHP (standalone, e.g. a shared webhost with no Composer):** drop in the `src/`folder and `autoload.php`, then `require __DIR__ . '/autoload.php';`.

**Node / TypeScript:**

```
npm install @t0xicvybez/gamequery
```

Ships both ESM and CommonJS, so `import` and `require()` both work. See [`node/`](node/) for the TypeScript API and CLI details.

Quick start
-----------

[](#quick-start)

**PHP:**

```
use GameQuery\GameQuery;

$gq = new GameQuery(timeoutMs: 2000, retries: 1);

$gq->addServer('source', '127.0.0.1:27015', id: 'my-css-server');
$gq->addServer('minecraft', 'mc.example.com:25565', id: 'survival');

// Protocols that need credentials take a per-server options bag — the same
// protocol instance serves every server, so nothing is baked into the class.
$gq->addServer('palworld', '203.0.113.10:8212', id: 'pal', options: [
    'password' => 'the-admin-password',
]);

foreach ($gq->process() as $result) {
    if (!$result->online) {
        // errorCode is a stable ErrorCode:: constant (TIMEOUT, UNREACHABLE, ...)
        echo "{$result->server->label()} is offline [{$result->errorCode}]\n";
        continue;
    }
    // Normalized accessors read the right field for any protocol.
    echo "{$result->name()}: {$result->players()}/{$result->maxPlayers()} players, {$result->pingMs}ms\n";
}
```

Just one server, or don't know the protocol? Skip the ceremony:

```
$r = GameQuery::queryOne('source', '127.0.0.1:27015');
$r = GameQuery::queryGame('rust', 'my-rust-server.com');   // resolves protocol + port
```

**Node / TypeScript:**

```
import { GameQuery } from '@t0xicvybez/gamequery';

const gq = new GameQuery(/* timeoutMs */ 2000, /* retries */ 1);
gq.addServer('source', '127.0.0.1:27015', 'my-css-server');
gq.addServer('minecraft', 'mc.example.com:25565');

for (const r of await gq.process()) {
  if (!r.online) {
    console.log(`${r.server.label()} is offline [${r.errorCode}]`);
    continue;
  }
  console.log(`${r.name()}: ${r.players()}/${r.maxPlayers()}, ${r.pingMs}ms`);
}

// Or a single server:
const one = await GameQuery.queryOne('source', '127.0.0.1:27015');
```

### Result API

[](#result-api)

- **Normalized accessors** — `name()`, `map()`, `players()`, `maxPlayers()`, `playerNames()`, and `playerList()` (structured `{name, score?, duration?}`) — read the right field regardless of protocol. Raw fields remain on `data`.
- **`errorCode`** — a stable `ErrorCode` value on failures (`TIMEOUT`, `UNREACHABLE`, `CONNECTION_CLOSED`, `AUTH_FAILED`, `PROTOCOL_ERROR`, `CONFIG_ERROR`) — switch on it instead of matching the human `error` string.
- **`maxConcurrent`** — an optional third constructor arg caps how many sockets are open at once (`new GameQuery(2000, 1, 256)`); use it for large fleets.
- **`queryWithPortProbe()`** — try a base port plus offsets and return the first that answers (for Source games whose query port is offset from the game port).
- **`queryOne()`** — query a single server without the addServer()/process() ceremony.
- **`queryGame()` / `addGame()`** — query by game id (`'rust'`, `'cs2'`, `'minecraft'`) instead of protocol; resolves the protocol + default port from a 53-game database (`gameInfo()` / `GAMES` expose it directly).
- **`gameInfo().gamePort`** — the port players *join* on, for games where it differs from the port that answers queries (Killing Floor 2 is queried on 27015 but joined on 7777). Advisory only and never queried — use it to show a correct connect address; it is omitted when both ports are the same.
- **`processStream()`** — an async iterator (PHP: a `Generator`) that yields each `Result` the moment its server answers, instead of waiting for the slowest.
- **`listServers()`** — discover Source/A2S servers via the Steam master server (returns `ip:port` strings to feed into `addServer('source', …)`).
- **`toArray()` / `toObject()`** — both serialize the result (the CLI's JSON shape).

Addresses accept IPv6 in bracket form (`[::1]:27015`). Every parser is fuzzed against malformed input, so a hostile or broken server reply can't crash it.

Results come back in add order, one per server. `data` holds whatever the protocol parsed — see each protocol class's `parse()` for its exact fields.

Supported protocols
-------------------

[](#supported-protocols)

**23 protocol families / 33 registered keys**, identical across both ports. Parenthesised keys are aliases or variants (e.g. `source-players` adds the player list, `-info` variants skip it).

KeyFamily / gamesTransport`source` (`source-players`, `source-full`)Source / A2S — CS2, TF2, Rust, ARK, GMod, SCUM, most Steamworks gamesUDP`minecraft` (`minecraft-ping`)Minecraft: Java Edition SLP (`-ping` adds 0x01 ping latency)TCP`minecraft-legacy`Minecraft: Java Edition (≤1.6 legacy ping)TCP`minecraft-query`Minecraft: Java `enable-query` (full player list)UDP`bedrock` (`minecraft-bedrock`)Minecraft: Bedrock Edition (RakNet)UDP`fivem` (`fivem-info`)FiveM / CFX (GTA V multiplayer)TCP/HTTP`palworld` (`palworld-info`)Palworld REST APITCP/HTTP`quakeworld` (`quake1`)QuakeWorld / Quake 1UDP`quake2`id Tech 2 / Quake 2UDP`quake3`id Tech 3 — Quake 3, CoD 1/2/4, OpenArena, Xonotic, ETUDP`gamespy1`GameSpy 1 — Unreal, early UT, Tribes 2, older titlesUDP`gamespy2`GameSpy 2 — Battlefield 1942/Vietnam, Halo, UT2004UDP`gamespy3`GameSpy 3 — Battlefield 2, Crysis, UT3, later titlesUDP`unreal2` (`unreal2-info`)Unreal Engine 2 — UT2003/2004, Killing FloorUDP`doom3`id Tech 4 — Doom 3, Quake 4, ET: Quake Wars, PreyUDP`ase`All-Seeing Eye — Multi Theft Auto (MTA:SA)UDP`mumble`Mumble / Murmur voice serversUDP`teamspeak3`TeamSpeak 3 / TeaSpeak (ServerQuery)TCP`frostbite`Battlefield 3/4, Bad Company 2, Medal of HonorTCP`assettocorsa`Assetto Corsa (HTTP `/INFO`)TCP/HTTP`terraria`Terraria via TShock RESTTCP/HTTP`samp` (`openmp`, `samp-info`)SA-MP / open.mp (GTA: San Andreas)UDP`satisfactory`Satisfactory Lightweight Query (name/state/build)UDPA few protocols need extra input: **Palworld** and **Terraria** take an admin password / token, **TeamSpeak 3** takes the voice port, and **Assetto Corsa**is queried on its HTTP port. Pass these through the per-server `options` bag — each protocol class's docblock lists the keys it reads.

Command-line interface
----------------------

[](#command-line-interface)

Both ports ship a `gamequery` CLI that emits JSON on stdout — handy for calling from any language, or from a shell:

```
# PHP
php bin/gamequery source 127.0.0.1:27015
php bin/gamequery palworld 203.0.113.10:8212 --password adminpw

# Node (after: npm i -g @t0xicvybez/gamequery)
gamequery minecraft mc.example.com:25565
gamequery --batch '[{"protocol":"source","address":"1.2.3.4:27015","id":"a"}]'
```

The `--batch` form takes a JSON array and queries every entry concurrently. When a password is involved, keep it off the process table: pass it via the `GAMEQUERY_PASSWORD` env var or `--password-stdin` rather than a `--password`flag (which is visible to anything that can run `ps`). The CLI always exits `0`(check the `online` field) unless there's a usage error.

Architecture
------------

[](#architecture)

```
GameQuery                facade: addServer() / process()
  ProtocolRegistry       name string -> protocol instance (33 keys)
  Server / Result        immutable value objects in, value objects out
  Transport/
    SocketManager        the concurrent event loop
    QuerySession         one server's state machine, driven by the loop
  Protocol/
    ProtocolInterface    the contract every game protocol implements
    AbstractProtocol     shared bookkeeping (tag lookup, defaults)
    Source, Minecraft, Palworld, … (23 protocol classes)
  Buffer/
    ByteReader / ByteWriter   binary (de)serialization helpers

```

**The core idea:** every game query protocol, however different its byte layout, is a *linear conversation* — send a packet, look at the reply, decide whether to send another. A protocol is just three methods (`initialStep`, `nextStep`, `parse`) describing that conversation; it never touches a socket. The transport layer owns every socket, timer, and retry and knows nothing about A2S or Minecraft specifically. That split is what keeps the concurrency engine small and makes adding a game a matter of writing one focused class.

Adding a protocol
-----------------

[](#adding-a-protocol)

1. Create the protocol class in **both** ports (`src/Protocol/YourGame.php` and `node/src/protocol/YourGame.ts`), extending `AbstractProtocol`.
2. Implement `transport()`, `initialStep()`, `nextStep()`, and `parse()`. See `Source` for a challenge/response example or `Minecraft` for a single-shot one.
3. Register it in both `ProtocolRegistry` files, or at runtime: ```
    $gq->registerProtocol('yourgame', fn() => new \GameQuery\Protocol\YourGame());
    ```
4. Add a crafted-packet test to both smoke suites.

No changes to the transport layer are ever needed for a new protocol. The PHP and Node ports must move together — see [CONTRIBUTING.md](CONTRIBUTING.md).

**Address resolution:** protocols whose request embeds the server's own numeric IP (SA-MP/open.mp) override `requiresAddressResolution()` to return `true`; the transport then resolves the host to an IPv4 address before `initialStep()` and exposes it via `Server::address()`. Protocols that don't opt in pay no cost.

Known limitations
-----------------

[](#known-limitations)

Kept deliberately honest — these are the edges worth knowing about:

- **A2S bzip2 decompression needs help in Node.** Multi-datagram `A2S_RULES`replies are reassembled, and the rare bzip2-*compressed* variant is decompressed automatically in PHP (when the `bz2` extension is present). Node has no built-in bzip2, so — to stay dependency-free — you supply one via `Source.setBzip2Decompressor(fn)`; without it, a compressed reply degrades to partial data rather than failing.
- **Minecraft `pingMs` is the status round trip.** For the `minecraft` protocol, `pingMs` is the handshake + status round trip. If you want the protocol's dedicated 0x01 ping as a purer network latency, use `minecraft-ping`, which reports it as `data.ping_ms` (at the cost of one extra round trip).
- **Palworld reads only, over `Content-Length` responses.** The read-only `info`/`players` GET endpoints are implemented; the mutating admin actions (kick/ban/shutdown) are intentionally out of scope. Response completion relies on `Content-Length` (which Palworld's Go-based REST API always sends) and both requests share one keep-alive connection — a server configured to close after each request would yield info-only data rather than an error. Use `palworld-info` if you only need server info.

Testing
-------

[](#testing)

```
php tests/smoke_test.php          # PHP unit suite (offline)
php tests/fuzz_test.php           # malformed-input fuzzer (offline)
cd node && npm test               # TS unit suite (build + run, offline)
cd node && npm run fuzz           # TS fuzzer

php tests/integration.php                 # optional live checks
cd node && npm run test:integration       # (edit the server list first)
```

Generate the API reference with `cd node && npm run docs` (typedoc) and `composer docs` (phpDocumentor — needs the phpDocumentor phar on your PATH).

The unit suites are dependency-free assertions against hand-built, known-good protocol byte sequences — buffer round-tripping, A2S parsing (including the 2020 `A2S_INFO` challenge and the "Ship" info variant), challenge hand-offs, Minecraft JSON framing, Palworld auth, and a crafted-packet case for every protocol. No network needed; both suites must stay all-green, and every protocol change lands in both ports with matching tests.

The `integration.php` / `integration.test.ts` scripts are separate, network-dependent diagnostics: they query real public servers and print a table, never failing the process on an offline server. Point them at your own servers to smoke-test a protocol end to end.

License
-------

[](#license)

[AGPL-3.0-or-later](LICENSE) © [@t0xicVybez](https://github.com/t0xicVybez)

GameQuery is free software and stays that way. You can use it, modify it and contribute to it freely; if you distribute a modified version — **including running one as a network service** — you have to make your source available under the same licence. That's what keeps this project open rather than something somebody repackages and sells closed.

**Every published release is AGPL.** The pre-0.5.4 releases, which were MIT, have been withdrawn from npm, Packagist and GitHub, so the AGPL is the only licence GameQuery is distributed under.

###  Health Score

36

—

LowBetter than 79% of packages

Maintenance91

Actively maintained with recent releases

Popularity8

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity33

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

43d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/170263409?v=4)[t0xicVybez](/maintainers/t0xicVybez)[@t0xicVybez](https://github.com/t0xicVybez)

---

Top Contributors

[![t0xicVybez](https://avatars.githubusercontent.com/u/170263409?v=4)](https://github.com/t0xicVybez "t0xicVybez (56 commits)")

---

Tags

a2sa2s-queryfivemgame-servergame-server-querygamedigminecraftnodejspalworldphprustserver-querysource-enginesource-querysteamtypescriptudpquerysteamsourceminecraftudpgame-servergameserverserver statusa2sgamedig

###  Code Quality

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/t0xicvybez-gamequery/health.svg)

```
[![Health](https://phpackages.com/badges/t0xicvybez-gamequery/health.svg)](https://phpackages.com/packages/t0xicvybez-gamequery)
```

###  Alternatives

[league/uri-components

URI components manipulation library

32048.1M110](/packages/league-uri-components)[anourvalar/eloquent-serialize

Laravel Query Builder (Eloquent) serialization

11228.1M38](/packages/anourvalar-eloquent-serialize)[tltneon/lgsl

PHP library retrieve game servers status from various types of games.

1991.6k1](/packages/tltneon-lgsl)[cakephp/datasource

Provides connection managing and traits for Entities and Queries that can be reused for different datastores

4727.7M17](/packages/cakephp-datasource)[coincheck/coincheck

Bindings of coincheck API

202.2k](/packages/coincheck-coincheck)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
