Skip to content

Adding a Protocol

Sascha Greuel edited this page Aug 13, 2026 · 2 revisions

Adding or updating a protocol

Home · Architecture · Tests and fixtures

Start by identifying whether the game uses an existing family. A small named subclass is safer than copying a mature parser.

Prefer an alias/subclass for compatible games

For a Source-compatible game, the class may need only metadata and a port offset:

<?php

namespace GameQ\Protocols;

/**
 * Example Game protocol.
 *
 * @author Your Name <you@example.com>
 */
class Examplegame extends Source
{
    protected string $name = 'examplegame';

    protected string $name_long = 'Example Game';

    protected int $port_diff = 1;
}

The filename and class are Examplegame.php / Examplegame: only the first character is capitalized because Server resolves ucfirst(strtolower($type)) on case-sensitive systems.

Confirm protocol compatibility from actual wire data and official documentation where available. Marketing claims such as “uses Steam” do not prove that a server implements A2S or uses a particular port.

New wire protocol checklist

A full protocol class normally defines:

  • $name, $name_long, $protocol, and $transport;
  • $state, initially STATE_BETA until representative fixtures exist;
  • $port_diff or an overridden findQueryPort();
  • $packets for initial requests;
  • a join-link template where a stable URI scheme exists;
  • a normalization map;
  • processResponse() with explicit validation;
  • beforeSend(), challenge handling, or getFollowUpPackets() only when required.

Return an empty array when a valid query establishes that the server is unavailable. Throw ProtocolException for malformed, contradictory, truncated, oversized, or unsupported responses. Do not turn malformed data into plausible partial fields silently.

Parser safety

  • Check headers, declared lengths, counts, offsets, and terminators before consuming data.
  • Use Buffer for binary parsing.
  • Bound response sizes, packet counts, decompression output, pagination, and loops.
  • Use JSON_THROW_ON_ERROR for JSON.
  • Use LIBXML_NONET and internal libxml error handling for untrusted XML.
  • Restrict cURL protocols, status codes, response sizes, redirects, and timeouts.
  • Do not use repeated array_merge() in parser loops; append values or assemble once.
  • Preserve ordering where a protocol uses sequence numbers or paginated offsets.
  • Avoid suppressing warnings unless the external API cannot be called safely another way and every return value is checked.

Shared-family changes

A change to Source, GameSpy, Quake, Unreal, Doom 3, Frostbite, RakNet, or another parent affects many games. Before changing shared behavior:

  1. identify every subclass;
  2. preserve initial packets and legacy branches that already work;
  3. add a fixture for the new case;
  4. run all child protocol tests;
  5. run composer bc-check for public/protected changes;
  6. document the behavior and compatibility impact.

Do not make a child protocol “correct” by breaking the parent's established children.

HTTP JSON and directory protocols

Extend JsonHttp when the configured game server exposes a simple, unauthenticated JSON status document over plain HTTP. Define the path, query-port offset, metadata, normalization, and processJson(array $data): array. The base constructs the HTTP request, supports IPv4 and IPv6 host headers, validates the HTTP response, decodes JSON with exceptions, and rejects scalar JSON documents.

Extend OfficialDirectory only when the game project operates a stable official HTTPS JSON directory. Implement directoryUrl(), findServer(), and processResponse(). Matching must include the advertised port and account for IPv4, IPv6, DNS names, and directory_address where appropriate.

Do not use either shared base for authenticated REST APIs, redirects, arbitrary HTTP methods, or multi-request workflows. Those require a purpose-built protocol implementation. See HTTP and directory protocols for the complete contracts and existing implementations.

Results and normalization

Retain useful native fields and map common values:

protected array $normalize = [
    'general' => [
        'hostname' => 'server_name',
        'mapname' => 'map',
        'numplayers' => 'player_count',
        'maxplayers' => 'max_players',
    ],
    'player' => [
        'name' => 'name',
        'score' => 'score',
    ],
];

Do not invent a universal meaning for a field that the protocol does not provide. Use null/absence rather than misleading zeroes or empty strings.

Tests and documentation

Every new identifier needs a test class, representative fixtures, malformed-input coverage for a new parser, and an entry in the changelog. Update:

Run the complete workflow in tests and fixtures.

Clone this wiki locally