Skip to content

Commit

Permalink
🔀 Merge pull request #1 from GiyoMoon/version/1.1.0
Browse files Browse the repository at this point in the history
Version/1.1.0
  • Loading branch information
GiyoMoon authored Dec 30, 2021
2 parents e3e27b9 + b5555fa commit 46311b1
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 31 deletions.
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![npm downloads](https://img.shields.io/npm/dm/steam-server-query.svg)](https://npmjs.com/package/steam-server-query)
[![license](https://img.shields.io/npm/l/steam-server-query.svg)](https://github.com/GiyoMoon/steam-server-query/blob/main/LICENSE)

Package which implements the [Master Server Query Protocol](https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol) and [Game Server Queries](https://developer.valvesoftware.com/wiki/Server_queries). It is working with promises.
Module which implements the [Master Server Query Protocol](https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol) and [Game Server Queries](https://developer.valvesoftware.com/wiki/Server_queries). It is working with promises.

## Install
```bash
Expand All @@ -13,36 +13,38 @@ npm install steam-server-query
## API
### Master Server Query
```javascript
queryMasterServer(masterServer: string, region: REGIONS, filter?: Filter, timeout?: number): Promise<string[]>
queryMasterServer(masterServer: string, region: REGIONS, filter?: Filter, timeout?: number, maxHosts?: number): Promise<string[]>
```
Fetch a Steam master server to retrieve a list of game server hosts.
- `masterServer`: Host and port of the master server to call.
- `region`: The region of the world where you wish to find servers in. Use `REGIONS.ALL` for all regions.
- `filter`: Optional. Object which contains filters to be sent with the query. Default is { }.
- `timeout`: Optional. Time in milliseconds after the socket request should fail. Default is 1 second.
- `maxHosts`: Optional. Return a limited amount of hosts. Stops calling the master server after this limit is reached. Can be used to prevent getting rate limited.
- Returns: A promise including an array of game server hosts.
### Game Server Query
```javascript
queryGameServerInfo(gameServer: string, timeout?: number): Promise<InfoResponse>
queryGameServerInfo(gameServer: string, attempts?: number, timeout?: number | number[]): Promise<InfoResponse>
```
Send a A2S_INFO request to a game server. Retrieves information like its name, the current map, the number of players and so on. Read more [here](https://developer.valvesoftware.com/wiki/Server_queries#A2S_INFO).

---
```javascript
queryGameServerPlayer(gameServer: string, timeout?: number): Promise<PlayerResponse>
queryGameServerPlayer(gameServer: string, attempts?: number, timeout?: number | number[]): Promise<PlayerResponse>
```
Send a A2S_PLAYER request to a game server. Retrieves the current playercount and for every player their name, score and duration. Read more [here](https://developer.valvesoftware.com/wiki/Server_queries#A2S_PLAYER).

---
```javascript
queryGameServerRules(gameServer: string, timeout?: number): Promise<RulesResponse>
queryGameServerRules(gameServer: string, attempts?: number, timeout?: number | number[]): Promise<RulesResponse>
```
Send a A2S_RULES request to a game server. Retrieves the rule count and for every rule its name and value. Read more [here](https://developer.valvesoftware.com/wiki/Server_queries#A2S_RULES).

---
Parameters for every Game Server Query function
- `gameServer`: Host and port of the game server to call.
- `timeout`: Optional. Time in milliseconds after the socket request should fail. Default is 1 second.
- `attempts`: Optional. Number of call attempts to make. Default is 1 attempt.
- `timeout`: Optional. Time in milliseconds after the socket request should fail. Default is 1000. Specify an array of timeouts if they should be different for every attempt. (Example for 3 attempts: `[1000, 1000, 2000]`)
- Returns: A promise including an object (Either type `InfoResponse`, `PlayerResponse` or `RulesResponse`)

## Types
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "steam-server-query",
"version": "1.0.0",
"description": "Package which implements the Master Server Query Protocol and Game Server Queries.",
"version": "1.1.0",
"description": "Module which implements the Master Server Query Protocol and Game Server Queries.",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"files": [
Expand Down
25 changes: 14 additions & 11 deletions src/gameServer/gameServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ import { InfoResponse, Player, PlayerResponse, Rule, RulesResponse } from './gam
*
* Read more [here](https://developer.valvesoftware.com/wiki/Server_queries#A2S_INFO).
* @param gameServer Host and port of the game server to call.
* @param timeout Optional. Time in milliseconds after the socket request should fail. Default is 1 second.
* @param attempts Optional. Number of call attempts to make. Default is 1 attempt.
* @param timeout Optional. Time in milliseconds after the socket request should fail. Default is 1000. Specify an array of timeouts if they should be different for every attempt.
* @returns A promise including an object of the type `InfoResponse`
*/
export async function queryGameServerInfo(gameServer: string, timeout = 1000): Promise<InfoResponse> {
export async function queryGameServerInfo(gameServer: string, attempts = 1, timeout: number | number[] = 1000): Promise<InfoResponse> {
const splitGameServer = gameServer.split(':');
const host = splitGameServer[0];
const port = parseInt(splitGameServer[1]);

const gameServerQuery = new GameServerQuery(host, port, timeout);
const gameServerQuery = new GameServerQuery(host, port, attempts, timeout);
const result = await gameServerQuery.info();
return result;
}
Expand All @@ -24,15 +25,16 @@ export async function queryGameServerInfo(gameServer: string, timeout = 1000): P
*
* Read more [here](https://developer.valvesoftware.com/wiki/Server_queries#A2S_PLAYER).
* @param gameServer Host and port of the game server to call.
* @param timeout Optional. Time in milliseconds after the socket request should fail. Default is 1 second.
* @param attempts Optional. Number of call attempts to make. Default is 1 attempt.
* @param timeout Optional. Time in milliseconds after the socket request should fail. Default is 1000. Specify an array of timeouts if they should be different for every attempt.
* @returns A promise including an object of the type `PlayerResponse`
*/
export async function queryGameServerPlayer(gameServer: string, timeout = 1000): Promise<PlayerResponse> {
export async function queryGameServerPlayer(gameServer: string, attempts = 1, timeout: number | number[] = 1000): Promise<PlayerResponse> {
const splitGameServer = gameServer.split(':');
const host = splitGameServer[0];
const port = parseInt(splitGameServer[1]);

const gameServerQuery = new GameServerQuery(host, port, timeout);
const gameServerQuery = new GameServerQuery(host, port, attempts, timeout);
const result = await gameServerQuery.player();
return result;
}
Expand All @@ -42,24 +44,25 @@ export async function queryGameServerPlayer(gameServer: string, timeout = 1000):
*
* Read more [here](https://developer.valvesoftware.com/wiki/Server_queries#A2S_RULES).
* @param gameServer Host and port of the game server to call.
* @param timeout Optional. Time in milliseconds after the socket request should fail. Default is 1 second.
* @param attempts Optional. Number of call attempts to make. Default is 1 attempt.
* @param timeout Optional. Time in milliseconds after the socket request should fail. Default is 1000. Specify an array of timeouts if they should be different for every attempt.
* @returns A promise including an object of the type `RulesResponse`
*/
export async function queryGameServerRules(gameServer: string, timeout = 1000): Promise<RulesResponse> {
export async function queryGameServerRules(gameServer: string, attempts = 1, timeout: number | number[] = 1000): Promise<RulesResponse> {
const splitGameServer = gameServer.split(':');
const host = splitGameServer[0];
const port = parseInt(splitGameServer[1]);

const gameServerQuery = new GameServerQuery(host, port, timeout);
const gameServerQuery = new GameServerQuery(host, port, attempts, timeout);
const result = await gameServerQuery.rules();
return result;
}

class GameServerQuery {
private _promiseSocket: PromiseSocket;

constructor(private _host: string, private _port: number, timeout: number) {
this._promiseSocket = new PromiseSocket(timeout);
constructor(private _host: string, private _port: number, attempts: number, timeout: number | number[]) {
this._promiseSocket = new PromiseSocket(attempts, timeout);
};

public async info(): Promise<InfoResponse> {
Expand Down
17 changes: 13 additions & 4 deletions src/masterServer/masterServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ const RESPONSE_START = Buffer.from([0xFF, 0xFF, 0xFF, 0xFF, 0x66, 0x0A]);
* @param region The region of the world where you wish to find servers in. Use REGIONS.ALL for all regions.
* @param filters Optional. Object which contains filters to be sent with the query. Default is { }. Read more [here](https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol#Filter).
* @param timeout Optional. Time in milliseconds after the socket request should fail. Default is 1 second.
* @param maxHosts Optional. Return a limited amount of hosts. Stops calling the master server after this limit is reached. Can be used to prevent getting rate limited.
* @returns A promise including an array of game server hosts.
*/
export async function queryMasterServer(masterServer: string, region: REGIONS, filters: Filter = {}, timeout = 1000): Promise<string[]> {
export async function queryMasterServer(masterServer: string, region: REGIONS, filters: Filter = {}, timeout = 1000, maxHosts?: number): Promise<string[]> {
const splitMasterServer = masterServer.split(':');
const host = splitMasterServer[0];
const port = parseInt(splitMasterServer[1]);

const masterServerQuery = new MasterServerQuery(host, port, region, filters, timeout);
const masterServerQuery = new MasterServerQuery(host, port, region, filters, timeout, maxHosts);
const hosts = await masterServerQuery.fetchServers();
return hosts;
}
Expand All @@ -29,8 +30,8 @@ class MasterServerQuery {
private _promiseSocket: PromiseSocket;
private _hosts: string[] = [];

constructor(private _host: string, private _port: number, private _region: REGIONS, private _filters: Filter, timeout: number) {
this._promiseSocket = new PromiseSocket(timeout);
constructor(private _host: string, private _port: number, private _region: REGIONS, private _filters: Filter, timeout: number, private _maxHosts?: number) {
this._promiseSocket = new PromiseSocket(1, timeout);
};

public async fetchServers() {
Expand All @@ -46,6 +47,14 @@ class MasterServerQuery {
const parsedHosts = this._parseBuffer(resultBuffer);
this._seedId = parsedHosts[parsedHosts.length - 1];
this._hosts.push(...parsedHosts);

if (
this._maxHosts &&
this._hosts.length >= this._maxHosts &&
this._hosts[this._maxHosts - 1] !== ZERO_IP
) {
return this._hosts.slice(0, this._maxHosts);
}
} while (this._seedId !== ZERO_IP);

// remove ZERO_IP from end of host list
Expand Down
41 changes: 35 additions & 6 deletions src/promiseSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,60 @@ import { createSocket, Socket } from 'dgram';
export class PromiseSocket {
private _socket: Socket;

constructor(private _timeout = 1000) {
constructor(private _attempts: number, private _timeout: number | number[]) {
if (
Array.isArray(this._timeout) &&
this._attempts !== this._timeout.length
) {
throw new Error(`Number of attempts (${this._attempts}) does not match the length of the timeout array (${this._timeout.length})`);
}
this._socket = createSocket('udp4');
}

public async send(buffer: Buffer, host: string, port: number): Promise<Buffer> {
return new Promise(async (resolve, reject) => {
for (let i = 0; i < this._attempts; i++) {
let timeout: number;
if (Array.isArray(this._timeout)) {
timeout = this._timeout[i];
} else {
timeout = this._timeout;
}

try {
const messageBuffer = await this._socketSend(buffer, host, port, timeout);
return resolve(messageBuffer);
} catch (err) {
if (i === this._attempts - 1) {
return reject(err);
}
}
}
});
}

private _socketSend(buffer: Buffer, host: string, port: number, timeout: number): Promise<Buffer> {
return new Promise((resolve, reject) => {
this._socket.send(buffer, port, host, (err) => {
if (err) return reject(typeof err == 'string' ? new Error(err) : err);

const messageListener = (buffer: any) => {
this._socket.removeListener('message', messageListener);
this._socket.removeListener('error', errorListener);
clearTimeout(timeout);
clearTimeout(timeoutFnc);
return resolve(buffer);
};

const errorListener = (err: Error) => {
reject(err);
clearTimeout(timeoutFnc);
return reject(err);
};

const timeout = setTimeout(() => {
const timeoutFnc = setTimeout(() => {
this._socket.removeListener('message', messageListener);
this._socket.removeListener('error', errorListener);
reject('Timeout reached. Possible reasons: You are being rate limited; Timeout too short; Wrong server host configured;');
}, this._timeout);
return reject('Timeout reached. Possible reasons: You are being rate limited; Timeout too short; Wrong server host configured;');
}, timeout);

this._socket.on('message', messageListener);
this._socket.on('error', errorListener);
Expand Down

0 comments on commit 46311b1

Please sign in to comment.