Skip to content

Adding a Protocol

Sascha Greuel edited this page Aug 2, 2026 · 1 revision

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.

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