@fabricio-191/valve-server-query

An implementation of valve protocols

Usage no npm install needed!

<script type="module">
  import fabricio191ValveServerQuery from 'https://cdn.skypack.dev/@fabricio-191/valve-server-query';
</script>

README

Buy Me A Coffee Discord

An implementation of valve protocols

const { Server, RCON, MasterServer } = require('@fabricio-191/valve-server-query');

This module allows you to:

  • Easily make queries to servers running valve games
  • Make queries to valve master servers
  • Use a remote console to control your server remotely

Some of the games where it works with are:

  • Counter-Strike
  • Counter-Strike: Global Offensive
  • Garry's Mod
  • Half-Life
  • Team Fortress 2
  • Day of Defeat
  • The ship
Options

These are the default values

{
  ip: 'localhost', //in MasterServer is 'hl2master.steampowered.com'
  port: 27015, //in MasterServer is 27011
  
  timeout: 2000,
  debug: false,
  enableWarns: true,

  //RCON
  password: 'The RCON password', // hasn't a default value
  
  //Master server
  quantity: 200,
  region: 'OTHER',
}


Server

const server = await Server({
  ip: '0.0.0.0',
  port: 27015,
  timeout: 3000,
});

const info = await server.getInfo();
console.log(info);

const players = await server.getPlayers();
console.log(players);

const rules = await server.getRules();
console.log(rules);

const ping = server.lastPing;
console.log(ping);

/*
You can also do:

const [info, players, rules] = await Promise.all([
  server.getInfo(),
  server.getPlayers().catch(e => {}),
  server.getRules().catch(e => {})
]);

This make all queries in parallel
*/
getInfo()

Performs an A2S_INFO query to the server

server.getInfo()
  .then(info => {
    console.log(info);
  })
  .catch(err => {
    //...
  });

Info can be something like this:

{
  address: '192.223.30.25:27015',
  ping: 222,
  protocol: 17,
  goldSource: false,
  name: '[Raidboss] Private KZ/Climb [GOKZ |Global | VIP/Whitelist Only]',
  map: 'kz_frozen_go',
  folder: 'csgo',
  game: 'Counter-Strike: Global Offensive',
  appID: 730n,
  players: { online: 3, max: 10, bots: 0 },
  type: 'dedicated',
  OS: 'linux',
  visibility: 'public',
  VAC: true,
  version: '1.37.6.9',
  //data after this may not be present
  port: 27015,
  steamID: 85568392922144671n,
  tv: {
    port: 27020,
    name: 'RaidbossTV'
  },
  keywords: [
    'empty',     '5v5',
    'boss',    'casual',
    'climb',     'comp',
    'competitive', 'esea',
    'faceit',    'gloves',
    'knife',     'kreedz',
    'kz',      'ladder',
    'priz',    'pub',
    'pug',     'raid',
    'raidboss',  'rank',
    'ranks',     'st'
  ],
  gameID: 730n
}

getPlayers()

Performs an A2S_PLAYER query to get the list of players in the server

Some servers may have disable this query (very rare).

server.getPlayers()
  .then(players => {
    console.log(`There are ${players.length} in the server`);

    const list = players
      .sort((a, b) => b.timeOnline - a.timeOnline)
      .map((player, index) => `${index + 1}. ${player.name} ${player.timeOnline}`)
      .join('\n');

    console.log(list);
  })
  .catch(console.error);

The time class has an personalized toString() and @@toPrimitive() methods

Example:

[
  {
    index: 0,
    name: 'kritikal',
    score: 0,
    timeOnline: Time {
      hours: 0,
      minutes: 10,
      seconds: 25,
      start: 2021-03-20T02:46:28.267Z, //Date
      raw: 625.2186279296875
    }
  },
  {
    index: 0,
    name: 'dmx;',
    score: 0,
    timeOnline: Time {
      hours: 0,
      minutes: 10,
      seconds: 25,
      start: 2021-03-20T02:46:28.267Z,
      raw: 625.0791625976562
    }
  },
  {
    index: 0,
    name: 'fenakz',
    score: 0,
    timeOnline: Time {
      hours: 0,
      minutes: 10,
      seconds: 21,
      start: 2021-03-20T02:46:28.271Z,
      raw: 621.9488525390625
    }
  },
  {
    index: 0,
    name: '[JC] Master-cba',
    score: 0,
    timeOnline: Time {
      hours: 0,
      minutes: 10,
      seconds: 15,
      start: 2021-03-20T02:46:28.278Z,
      raw: 615.4395751953125
    }
  },
  {
    index: 0,
    name: 'sAIONARAH34! [pw] ⚓✵♣',
    score: 1,
    timeOnline: Time {
      hours: 0,
      minutes: 10,
      seconds: 1,
      start: 2021-03-20T02:46:28.292Z,
      raw: 601.7681274414062
    }
  },
  {
    index: 0,
    name: 'INFRA-',
    score: 0,
    timeOnline: Time {
      hours: 0,
      minutes: 9,
      seconds: 50,
      start: 2021-03-20T02:46:28.303Z,
      raw: 590.30859375
    }
  },
  {
    index: 0,
    name: 'Agente86',
    score: 1,
    timeOnline: Time {
      hours: 0,
      minutes: 9,
      seconds: 0,
      start: 2021-03-20T02:46:28.353Z,
      raw: 540.4190063476562
    }
  }
]

If the game is The Ship every player will have 2 extra properties deaths and money


getRules()

Makes an A2S_RULES query to the server

Some servers may have disable this query, so you should expect an error.

server.getRules()
  .then(console.log)
  .catch(() => {});

The response can change a lot between servers

Example:

{
  bot_quota: 0,
  coop: 0,
  cssdm_enabled: 0,
  cssdm_ffa_enabled: 0,
  cssdm_version: '2.1.6-dev',
  deathmatch: 1,
  decalfrequency: 10,
  gungame_enabled: 1,
  metamod_version: '1.10.7-devV',
  mp_allowNPCs: 1,
  mp_autocrosshair: 1,
  mp_autoteambalance: 1,
  mp_c4timer: 35,
  mp_disable_respawn_times: 0,
  mp_fadetoblack: 0,
  mp_falldamage: 0,
  mp_flashlight: 1,
  mp_footsteps: 1,
  mp_forceautoteam: 0,
  mp_forcerespawn: 1,
  mp_fraglimit: 0,
  mp_freezetime: 1,
  mp_friendlyfire: 0,
  mp_holiday_nogifts: 0,
  mp_hostagepenalty: 13,
  mp_limitteams: 2,
  mp_match_end_at_timelimit: 0,
  mp_maxrounds: 0,
  mp_respawnwavetime: 10,
  mp_roundtime: 9,
  mp_scrambleteams_auto: 1,
  mp_scrambleteams_auto_windifference: 2,
  mp_stalemate_enable: 0,
  mp_stalemate_meleeonly: 0,
  mp_startmoney: 800,
  mp_teamlist: 'hgrunt;scientist',
  mp_teamplay: 0,
  mp_timelimit: 18,
  mp_tournament: 0,
  mp_weaponstay: 0,
  mp_winlimit: 0,
  nextlevel: '',
  r_AirboatViewDampenDamp: 1,
  r_AirboatViewDampenFreq: 7,
  r_AirboatViewZHeight: 0,
  r_JeepViewDampenDamp: 1,
  r_JeepViewDampenFreq: 7,
  r_JeepViewZHeight: 10,
  r_VehicleViewDampen: 1,
  scc_version: '2.0.0',
  sm_advertisements_version: 0.6,
  sm_allchat_version: '1.1.1',
  sm_cannounce_version: 1.8,
  sm_ggdm_version: '1.8.0',
  sm_gungamesm_version: '1.2.16.0',
  sm_nextmap: 'gg_toon_poolday',
  sm_noblock: 1,
  sm_playersvotes_version: '1.5.0',
  sm_quakesounds_version: 1.8,
  sm_resetscore_version: '2.6.0',
  sm_show_damage_version: '1.0.7',
  sm_vbping_version: 1.4,
  sourcemod_version: '1.10.0.6482',
  sv_accelerate: 5,
  sv_airaccelerate: 10,
  sv_allowminmodels: 1,
  sv_alltalk: 1,
  sv_bounce: 0,
  sv_cheats: 0,
  sv_competitive_minspec: 0,
  sv_contact: 'linkinaz0@vtr.net',
  sv_enableboost: 0,
  sv_enablebunnyhopping: 0,
  sv_footsteps: 1,
  sv_friction: 4,
  sv_gravity: 800,
  sv_maxspeed: 320,
  sv_maxusrcmdprocessticks: 24,
  sv_noclipaccelerate: 5,
  sv_noclipspeed: 5,
  sv_nostats: 0,
  sv_password: 0,
  sv_pausable: 0,
  sv_rollangle: 0,
  sv_rollspeed: 200,
  sv_specaccelerate: 5,
  sv_specnoclip: 1,
  sv_specspeed: 3,
  sv_steamgroup: '',
  sv_stepsize: 18,
  sv_stopspeed: 75,
  sv_tags: 'alltalk',
  sv_voiceenable: 1,
  sv_vote_quorum_ratio: 0.6,
  sv_wateraccelerate: 10,
  sv_waterfriction: 1,
  tf_arena_max_streak: 3,
  tf_arena_preround_time: 10,
  tf_arena_round_time: 0,
  tf_arena_use_queue: 1,
  tv_enable: 0,
  tv_password: 0,
  tv_relaypassword: 0
  }

ping()

Performs an A2A_PING query into the server

This is a deprecated feature of source servers, may not work. The server.lastPing property contains the server ping, so this is not necessary

A warn in console will be shown (you can disable it by using { enableWarns: false }, see Options)

It will return -1 if the server did not respond to the query

server.ping()
  .then(ping => {
    console.log(ping); // 214
  })
  .catch(console.error)

disconnect()

Disconnect the server and destroy the socket

server.disconnect();

Use example

Server(...)
  .then(async server => {
    const players = await server.getPlayers();

    server.disconnect();

    //...
  })
  .catch(console.error);
static getInfo()
The difference is that it does not require the extra step of connection

Returns a promise that is resolved in an object with the server information, example:

const { Server } = require('@fabricio-191/valve-server-query');

Server.getInfo({
  ip: '0.0.0.0',
  port: 27015,
})
  .then(console.log)
  .catch(console.error);

MasterServer

Warns:

  • Gold source master servers may not work. The reason is unknown, the servers simply do not respond to queries
  • If the quantity is Infinity or 'all', it will try to get all the servers, but most likely it will end up giving an warning (it will as many servers as possible). The master server stops responding at a certain point.
MasterServer({
  quantity: 1000, // or Infinity or 'all'
  region: 'US_EAST',
  timeout: 3000,
})
  .then(servers => {
    //do something...

    //servers is an array if 'ip:port' strings, see below
  })
  .catch(console.error);

Valid regions are

  • US_EAST
  • US_WEST
  • SOUTH_AMERICA
  • EUROPE
  • ASIA
  • AUSTRALIA
  • MIDDLE_EAST
  • AFRICA
  • OTHER
Response example
[
  '190.195.150.143:27015', '143.255.142.150:27015', '177.54.144.122:27523',
  '189.1.173.26:27015',  '177.144.128.13:27015',  '177.66.222.92:27015',
  '196.28.69.113:27065',   '144.48.37.119:27015',   '139.180.174.191:27051',
  '108.61.168.31:27050',   '108.61.168.31:27015',   '108.61.168.31:27051',
  '139.180.174.191:27052', '139.180.174.191:27053', '139.99.173.74:27015',
  '45.121.210.91:27550',   '139.99.131.105:27015',  '139.99.144.39:27015',
  '34.87.217.246:27015',   '108.61.227.50:27025',   '108.61.227.12:27035',
  '220.240.1.134:27023',   '221.121.159.236:35240', '221.121.159.236:35260',
  '221.121.159.236:35250', '221.121.149.12:32860',  '121.74.206.225:27015',
  '41.190.141.250:27015',  '111.221.44.137:27018',  '111.221.44.137:27024',
  '111.221.44.137:27023',  '49.245.116.134:27017',  '49.245.116.134:27025',
  '49.245.116.134:27027',  '49.245.116.134:27015',  '49.245.116.134:27016',
  '49.245.116.134:27026',  '223.25.71.43:27015',  '49.245.116.134:27115',
  '49.245.116.134:27118',  '49.245.116.134:27117',  '49.245.116.134:27116',
  '13.229.55.66:24000',  '49.245.116.134:27215',  '103.9.159.78:27065',
  '75.85.184.227:27031',   '75.85.184.227:27034',   '168.235.81.229:27015',
  '66.55.74.111:27015',  '66.55.74.100:27015',  '66.55.74.82:27015',  
  '66.55.74.38:27015',   '66.55.74.103:27015',  '66.55.68.38:27015',  
  '66.55.74.65:27015',   '66.55.74.105:27015',  '66.55.74.104:27015',
  '66.55.68.36:27015',   '66.55.70.53:27015',   '64.111.99.165:27020',
  '66.55.70.177:27115',  '66.55.70.177:27315',  '104.153.109.22:27015',
  '104.153.109.26:27015',  '47.153.235.28:27015',   '173.199.84.186:27015',
  '64.190.203.117:27015',  '103.214.108.12:27105',  '173.199.87.235:27025',
  '8.3.6.148:27015',     '66.75.2.253:27015',   '172.107.198.173:27075',
  '172.107.2.177:27035',   '198.12.71.30:27015',  '206.251.72.62:27017',
  '104.207.148.159:27045', '92.38.148.25:27015',  '159.89.142.219:27016',
  '159.89.142.219:27015',  '159.89.142.219:27017',  '74.91.118.231:27015',
  '108.61.124.77:27065',   '192.53.126.95:27015',   '108.61.124.78:27980',
  '108.61.235.138:27015',  '108.61.124.72:27045',   '104.206.244.2:19001',
  '198.24.171.83:27185',   '131.153.29.243:27035',  '173.27.92.73:27016',
  '162.248.90.33:27015',   '162.248.90.38:27015',   '162.248.90.19:27015',
  '66.58.130.164:27015',   '71.193.199.206:27015',  '71.193.199.206:27017',
  '54.202.134.208:27015',  '64.42.176.58:27015',  '104.192.227.146:17741',
  '74.201.72.18:27015',
  ...1051 more items
]
filter
const filter = new MasterServer.Filter()
  .addFlag('dedicated')
  .add('map', 'cs_italy')
  .addNOR(
    new MasterServer.Filter()
      .addFlag('secure')
  );

MasterServer(
  quantity: 1000,
  region: 'EUROPE',
  timeout: 3000,
  filter,
})
  .then(console.log)
  .catch(console.error)

// Will return a list of ips that are dedicated servers, in the cs_italy map and that do not have an anti-cheat enabled

add(key, value) adds a condition to the filter. addFlag(flag) adds a flag to the filter. addFlags([flag, flag, ...]) adds multiple flags to the filter. addNOR(filter) a special condition, specifies that servers matching any of the filter conditions should not be returned. addNAND(filter) a special condition, specifies that servers matching all of the filter conditions should not be returned.

See https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol#Filter

Parameter Values Description
name_match string Servers with their hostname matching that hostname (can use * as a wildcard)
version_match string Servers running version that version (can use * as a wildcard)
gameaddr string Return only servers on the specified IP address (port supported and optional)
gamedir string Servers running the specified modification (ex. cstrike)
map string Servers running the specified map (ex. cs_italy)
appid number Servers that are running game with that appid
napp number Servers that are NOT running game with that appid
gametype array Servers with all of the given tag(s) in sv_tags
gamedata array Servers with all of the given tag(s) in their 'hidden' tags (only in L4D2)
gamedataor array Servers with any of the given tag(s) in their 'hidden' tags (only in L4D2)

Notes:

  • all array's are of strings.
  • flags are not booleans. if you want to get the inverse of any of these flags, you should use addNOR or addNAND.

Flags

Name Description
dedicated Servers that are dedicated servers
secure Servers using anti-cheat technology (VAC, but potentially others as well)
linux Servers running on a Linux platform
password Servers that are not password protected
noplayers Servers that are empty
empty Servers that are not empty
full Servers that are not full
proxy Servers that are spectator proxies
white Servers that are whitelisted
collapse_addr_hash Return only one server for each unique IP address matched
getIPS()
MasterServer.getIPS()
  .then(console.log)
  .catch(console.error)

/*
Returns an object with the master servers ips, like this: 
{
  goldSource: [ '208.64.200.118', '208.64.200.117' ],  
  source: [ '208.64.200.65', '208.64.200.39', '208.64.200.52' ]
}
*/

See https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol#Master_servers

The port used in hl2master.steampowered.com (source) ip's is 27011 but one of them is using a different port: 27015
The port numbers used by hl1master.steampowered.com (goldSource) can be anything between 27010 and 27013.


RCON

Some commands will cause the server to disconnect, for example sv_gravity 0 in some cases or rcon_password newPassword as it changes de password and needs to authenticate again.

Commands list

const rcon = await RCON({
  ip: '0.0.0.0',
  port: 27015, //RCON port
  password: 'your RCON password'
});

rcon.on('disconnect', async (reason) => {
  console.log('disconnected', reason);
  try{
    await rcon.reconnect();
  }catch(e){
    console.log('reconnect failed', e.message);
  }
});

rcon.on('passwordChanged', async () => {
  const password = await getNewPasswordSomehow();
  try{
    await rcon.authenticate(password);
  }catch(e){
    console.error('Failed to authenticate with new password', e.message);
  }
});

const response = await rcon.exec('sv_gravity 1000');
// Response is always a string that is some kind of log of the server or it can be empty
exec(command)

This will work well with server.getRules()

// gravity will change randomly every 5 seconds

setInterval(() => {
  const value = Math.floor(Math.random() * 10000) - 3000;
  //value will be a number between -3000 and 6999

  rcon.exec(`sv_gravity ${value}`)
    .catch(console.error);

}, 5000);
destroy()

Destroys the RCON connection, this will not fire the disconnect event

rcon.destroy();