Skip to content

HLTVProxy/typescript-a2s

Repository files navigation

TypeScript A2S

ALL CODE GENERATED BY VIBE CODING

A modern, fully-typed TypeScript library for querying Source and GoldSource game servers using Valve's A2S (Application-level Server Query) protocol. This is a complete TypeScript/Node.js implementation inspired by the original python-a2s library.

Features

  • 🚀 Full TypeScript Support: Complete type definitions with generics and strict typing
  • 🎯 Comprehensive Queries: Server info, player lists, and server rules/configuration
  • 🔧 Multi-Engine Support: Both Source Engine and GoldSource Engine compatibility
  • 📦 Modern Build System: Built with Vite for optimal performance and tree-shaking
  • 🌐 Universal Compatibility: ESM and CommonJS support for all Node.js environments
  • 🛡️ Robust Error Handling: Custom exception types with detailed error information
  • Async/Promise API: Modern Promise-based interface with async/await support
  • 🔤 Flexible Encoding: UTF-8, Latin1, or raw Buffer support for international servers
  • 🧩 Advanced Utilities: ByteReader/ByteWriter for custom protocol implementations
  • 🔀 Multi-packet Support: Automatic handling of fragmented server responses
  • 🗜️ Compression Support: Built-in bz2 decompression for compressed responses
  • 🧪 Fully Tested: Comprehensive test suite with 100% coverage

Installation

npm install typescript-a2s
# or
yarn add typescript-a2s
# or
pnpm add typescript-a2s

Quick Start

import { info, players, rules } from 'typescript-a2s';

// Query server information
const serverInfo = await info('127.0.0.1', 27015);
console.log(`Server: ${serverInfo.serverName}`);
console.log(`Map: ${serverInfo.mapName}`);
console.log(`Players: ${serverInfo.playerCount}/${serverInfo.maxPlayers}`);
console.log(`Ping: ${serverInfo.ping}ms`);

// Query connected players
const playerList = await players('127.0.0.1', 27015);
playerList.forEach((player) => {
  const duration = Math.floor(player.duration / 60);
  console.log(`${player.name}: ${player.score} points (${duration}m)`);
});

// Query server configuration
const serverRules = await rules('127.0.0.1', 27015);
console.log(
  `Server has ${Object.keys(serverRules).length} configuration rules`
);

API Reference

Core Functions

info(address: string, port: number, timeout?: number, encoding?: string | null): Promise<SourceInfo | GoldSrcInfo>

Queries comprehensive server information including name, map, player counts, and technical details.

Parameters:

  • address - Server IP address or hostname
  • port - Server query port (usually game port + 1)
  • timeout - Request timeout in seconds (default: 3.0)
  • encoding - String encoding: 'utf-8', 'latin1', or null for raw Buffer (default: 'utf-8')

Returns: Promise resolving to engine-specific server information

players(address: string, port: number, timeout?: number, encoding?: string | null): Promise<Player[]>

Retrieves a list of currently connected players with scores and connection times.

Parameters: Same as info()

Returns: Promise resolving to an array of player objects

rules(address: string, port: number, timeout?: number, encoding?: string | null): Promise<Rules>

Fetches server configuration variables and custom settings.

Parameters: Same as info()

Returns: Promise resolving to a key-value object of server rules

Advanced Client Class

A2SClient

Advanced client class for persistent connections and batch queries.

import { A2SClient } from 'typescript-a2s';

const client = new A2SClient('127.0.0.1', 27015, 5000, 'utf-8');

try {
  // Do NOT use Promise.all with a single A2SClient instance!
  // The A2S protocol and UDP socket are not concurrency-safe.
  // Always await each query sequentially to avoid protocol errors.
  const serverInfo = await client.info();
  const playerList = await client.players();
  const serverRules = await client.rules();

  console.log('Query results:', {
    server: serverInfo.serverName,
    players: playerList.length,
    rules: Object.keys(serverRules).length,
  });
} finally {
  client.close(); // Always clean up resources
}

Constructor Parameters:

  • address - Server IP address or hostname
  • port - Server query port
  • timeout - Request timeout in milliseconds (default: 3000)
  • encoding - String encoding (default: 'utf-8')

Type Definitions

SourceInfo - Source Engine Servers

interface SourceInfo {
  protocol: number;
  serverName: string | Buffer;
  mapName: string | Buffer;
  folder: string | Buffer;
  game: string | Buffer;
  appId: number;
  playerCount: number;
  maxPlayers: number;
  botCount: number;
  serverType: string | Buffer;
  platform: string | Buffer;
  passwordProtected: boolean;
  vacEnabled: boolean;
  version: string | Buffer;
  edf: number;
  ping: number;

  // Optional extended fields
  port?: number;
  steamId?: bigint;
  stvPort?: number;
  stvName?: string | Buffer;
  keywords?: string | Buffer;
  gameId?: bigint;
}

GoldSrcInfo - GoldSource Engine Servers

interface GoldSrcInfo {
  address: string | Buffer;
  serverName: string | Buffer;
  mapName: string | Buffer;
  folder: string | Buffer;
  game: string | Buffer;
  playerCount: number;
  maxPlayers: number;
  protocol: number;
  serverType: string | Buffer;
  platform: string | Buffer;
  passwordProtected: boolean;
  isMod: boolean;
  vacEnabled: boolean;
  botCount: number;
  ping: number;

  // Optional mod information
  modWebsite?: string | Buffer;
  modDownload?: string | Buffer;
  modVersion?: number;
  modSize?: number;
  multiplayerOnly?: boolean;
  usesCustomDll?: boolean;
}

Player<T> - Player Information

interface Player<T = string> {
  index: number; // Player slot index
  name: T; // Player name (string or Buffer based on encoding)
  score: number; // Player score/kills
  duration: number; // Connection time in seconds
}

Rules<T> - Server Configuration

type Rules<T = string> = Record<string, T>;

Exception Handling

Custom Exception Types

// Protocol-level errors
class BrokenMessageError extends Error {
  // Thrown when server response is malformed or invalid
}

// Buffer operation errors
class BufferExhaustedError extends BrokenMessageError {
  // Thrown when trying to read beyond buffer boundaries
}

Error Handling Example

import { info, BrokenMessageError, BufferExhaustedError } from 'typescript-a2s';

try {
  const serverInfo = await info('127.0.0.1', 27015);
  console.log(serverInfo);
} catch (error) {
  if (error instanceof BrokenMessageError) {
    console.error('Server returned invalid data:', error.message);
  } else if (error instanceof BufferExhaustedError) {
    console.error('Data parsing error:', error.message);
  } else if (error.code === 'ECONNREFUSED') {
    console.error('Server is offline or unreachable');
  } else if (error.code === 'ETIMEDOUT') {
    console.error('Query timed out - server may be overloaded');
  } else {
    console.error('Unexpected error:', error);
  }
}

Advanced Utilities

Binary Data Processing

import { ByteReader, ByteWriter } from 'typescript-a2s';

// Reading binary data
const reader = new ByteReader(buffer, true, 'utf-8');
const value = reader.readUint32();
const text = reader.readString();

// Writing binary data
const writer = new ByteWriter();
writer.writeUint32(12345);
writer.writeString('Hello World');
const result = writer.toBuffer();

Constants and Defaults

import {
  DEFAULT_TIMEOUT, // 3.0 seconds
  DEFAULT_ENCODING, // 'utf-8'
  DEFAULT_RETRIES, // 5 attempts
} from 'typescript-a2s';

Usage Examples

Basic Server Monitoring

import { info, players, rules } from 'typescript-a2s';

async function monitorServer(address: string, port: number) {
  try {
    // This is safe because info() and players() are stateless functions.
    // Do NOT use Promise.all with a single A2SClient instance!
    const [serverInfo, playerList] = await Promise.all([
      info(address, port),
      players(address, port),
    ]);

    console.log(`📊 ${serverInfo.serverName}`);
    console.log(`🗺️  Map: ${serverInfo.mapName}`);
    console.log(
      `👥 Players: ${serverInfo.playerCount}/${serverInfo.maxPlayers}`
    );
    console.log(`🏓 Ping: ${(serverInfo.ping * 1000).toFixed(1)}ms`);

    if (playerList.length > 0) {
      console.log('\n🎮 Top Players:');
      playerList
        .sort((a, b) => b.score - a.score)
        .slice(0, 5)
        .forEach((player, i) => {
          const time = Math.floor(player.duration / 60);
          console.log(
            `  ${i + 1}. ${player.name} - ${player.score} (${time}m)`
          );
        });
    }
  } catch (error) {
    console.error(`❌ Failed to query ${address}:${port}`, error.message);
  }
}

await monitorServer('127.0.0.1', 27015);

Multi-Server Batch Queries

import { A2SClient } from 'typescript-a2s';

const servers = [
  { name: 'Server 1', host: '127.0.0.1', port: 27015 },
  { name: 'Server 2', host: '127.0.0.1', port: 27016 },
  { name: 'Server 3', host: '127.0.0.1', port: 27017 },
];

async function queryMultipleServers() {
  const results = await Promise.allSettled(
    servers.map(async (server) => {
      const client = new A2SClient(server.host, server.port, 3000);
      try {
        const info = await client.info();
        return {
          server: server.name,
          name: info.serverName,
          players: info.playerCount,
          maxPlayers: info.maxPlayers,
          ping: info.ping,
        };
      } finally {
        client.close();
      }
    })
  );

  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      const data = result.value;
      console.log(
        `✅ ${data.server}: ${data.players}/${data.maxPlayers} players`
      );
    } else {
      console.log(`❌ ${servers[index].name}: ${result.reason.message}`);
    }
  });
}

await queryMultipleServers();

Encoding and Internationalization

import { info } from 'typescript-a2s';

async function testEncodings(address: string, port: number) {
  // UTF-8 encoding (default)
  const utf8Info = await info(address, port, 3000, 'utf-8');
  console.log('UTF-8 server name:', utf8Info.serverName);

  // Latin1 encoding for older servers
  const latin1Info = await info(address, port, 3000, 'latin1');
  console.log('Latin1 server name:', latin1Info.serverName);

  // Raw binary data (no string decoding)
  const rawInfo = await info(address, port, 3000, null);
  console.log('Raw server name buffer:', rawInfo.serverName);

  // Convert raw buffer to string manually if needed
  if (Buffer.isBuffer(rawInfo.serverName)) {
    const decoded = rawInfo.serverName.toString('utf-8');
    console.log('Manually decoded:', decoded);
  }
}

await testEncodings('127.0.0.1', 27015);

Real-time Server Monitoring

import { info } from 'typescript-a2s';

class ServerMonitor {
  private interval: NodeJS.Timeout | null = null;
  private lastPlayerCount = -1;

  async start(address: string, port: number, intervalMs = 10000) {
    console.log(`🔄 Starting monitor for ${address}:${port}`);

    this.interval = setInterval(async () => {
      try {
        const serverInfo = await info(address, port, 2000);

        if (serverInfo.playerCount !== this.lastPlayerCount) {
          const timestamp = new Date().toLocaleTimeString();
          const change =
            this.lastPlayerCount === -1
              ? ''
              : ` (${serverInfo.playerCount > this.lastPlayerCount ? '+' : ''}${
                  serverInfo.playerCount - this.lastPlayerCount
                })`;

          console.log(
            `[${timestamp}] ${serverInfo.serverName}: ` +
              `${serverInfo.playerCount}/${serverInfo.maxPlayers}${change}`
          );

          this.lastPlayerCount = serverInfo.playerCount;
        }
      } catch (error) {
        console.error(
          `[${new Date().toLocaleTimeString()}] Error:`,
          error.message
        );
      }
    }, intervalMs);
  }

  stop() {
    if (this.interval) {
      clearInterval(this.interval);
      this.interval = null;
      console.log('🛑 Monitor stopped');
    }
  }
}

const monitor = new ServerMonitor();
await monitor.start('127.0.0.1', 27015);

// Stop monitoring on Ctrl+C
process.on('SIGINT', () => {
  monitor.stop();
  process.exit(0);
});

Important Notes & Best Practices

Server Behavior Variations

  • Data Consistency: Some servers may return inconsistent or incomplete data. Always validate critical fields before use.
  • Player Count Discrepancies: The playerCount in server info may not exactly match the length of the players array due to server implementation differences.
  • Port Configuration: Query ports often differ from game connection ports. Common patterns include game port + 1, or separate configured query ports.

Protocol Limitations

  • Player Limits: The A2S protocol cannot return more than 255 players due to byte field limitations.
  • Unicode Support: String encoding varies by server configuration. Use appropriate encoding settings for international servers.
  • Rate Limiting: This library does not implement rate limiting. Implement appropriate delays between requests to avoid being blocked.

Performance Considerations

  • Connection Management: Use A2SClient class for multiple queries to the same server to reuse connections.
  • Timeout Tuning: Adjust timeout values based on network conditions and server responsiveness.
  • Error Handling: Always implement proper error handling, especially for production monitoring systems.

Supported Games & Engines

Source Engine Games

  • Half-Life 2
  • Team Fortress 2
  • Counter-Strike: Global Offensive
  • Counter-Strike: Source
  • Left 4 Dead 2
  • Portal 2
  • Garry's Mod
  • Day of Defeat: Source

GoldSource Engine Games

  • Half-Life
  • Counter-Strike 1.6
  • Day of Defeat
  • Team Fortress Classic
  • Ricochet

Development

Project Setup

# Clone the repository
git clone https://github.com/Yepoleb/python-a2s.git
cd python-a2s/typescript-a2s

# Install dependencies
pnpm install

# Run development server
pnpm dev

Build Commands

# Build for production
pnpm build

# Generate type declarations
pnpm build:types

# Run tests
pnpm test

# Run tests with UI
pnpm test:ui

# Generate coverage report
pnpm test:coverage

# Lint code
pnpm lint

# Fix linting issues
pnpm lint:fix

# Run example
pnpm example

Project Structure

typescript-a2s/
├── src/
│   ├── a2s.ts           # Core client implementation
│   ├── socket.ts        # UDP network layer
│   ├── protocols.ts     # A2S protocol handlers
│   ├── fragment.ts      # Multi-packet response handling
│   ├── byteio.ts        # Binary data processing
│   ├── types.ts         # TypeScript definitions
│   ├── exceptions.ts    # Error classes
│   ├── defaults.ts      # Configuration constants
│   ├── index.ts         # Public API exports
│   └── __tests__/       # Test suite
├── examples/            # Usage examples
├── dist/               # Built output
└── docs/               # Documentation

Requirements

  • Node.js: 18.0.0 or higher
  • TypeScript: 5.0.0 or higher (for development)
  • Dependencies: Zero runtime dependencies

License

MIT License - see LICENSE file for details.

Contributing

Contributions are welcome! Please read CONTRIBUTING.md for guidelines.

Related Projects

Acknowledgments

  • Based on the excellent python-a2s library by Yepoleb
  • Valve Corporation for the A2S protocol specification
  • The TypeScript and Node.js communities for excellent tooling

About

No description, website, or topics provided.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published