From c82554ad1a6ab5b694bbce4da54bc92c10ff729a Mon Sep 17 00:00:00 2001 From: Michael Morrison Date: Fri, 31 Jan 2014 16:27:52 -0600 Subject: [PATCH] Super mega-commit Organize files Rewrite readme for new game IDs and command line Add command line access Replace some dependencies that required binaries with simpler alternatives Switch gbxremote back to upstream, Closes #2 Moved simple aliases into an alias file, rather than seperate files for each Patched nearly every protocol variant with tons of bug fixes Re-tested every combination of server and protocol types except nadeo Added alternative minecraft query check (minecraftping) Fixed mutant factions query Fixed valve gold not working at all Stripped colors more reliably from protocols that support colors Added a couple more fields to ut2004 and killing floor and more that I probably forgot. This shouldn't break compatibility too bad -- at the most, some game IDs may have changed. --- .gitignore | 1 + README.md | 115 ++++++++++------ bin/gamedig.js | 27 ++++ games/aliases.txt | 29 ++++ {protocols => games}/armagetron.js | 14 +- {protocols => games}/buildandshoot.js | 2 +- games/jcmp.js | 30 +++++ games/killingfloor.js | 13 ++ {protocols => games}/minecraft.js | 2 +- games/minecraftping.js | 127 ++++++++++++++++++ {protocols => games}/mutantfactions.js | 4 +- {protocols => games/protocols}/core.js | 6 +- {protocols => games/protocols}/gamespy3.js | 38 ++++-- {protocols => games/protocols}/nadeo.js | 0 {protocols => games/protocols}/quake2.js | 0 games/protocols/quake3.js | 21 +++ {protocols => games/protocols}/unreal2.js | 48 +++++-- .../source.js => games/protocols/valve.js | 51 +++++-- games/protocols/valvegold.js | 6 + protocols/tshock.js => games/terraria.js | 2 +- games/ut2004.js | 12 ++ {protocols => games}/ut3.js | 2 +- {protocols => games}/warsow.js | 4 +- Class.js => lib/Class.js | 0 index.js => lib/index.js | 17 ++- reader.js => lib/reader.js | 44 ++++-- lib/typeresolver.js | 51 +++++++ package.json | 19 ++- protocols/killingfloor.js | 7 - protocols/quake3.js | 9 -- protocols/ut2004.js | 7 - 31 files changed, 573 insertions(+), 135 deletions(-) create mode 100644 .gitignore create mode 100644 bin/gamedig.js create mode 100644 games/aliases.txt rename {protocols => games}/armagetron.js (80%) rename {protocols => games}/buildandshoot.js (96%) create mode 100644 games/jcmp.js create mode 100644 games/killingfloor.js rename {protocols => games}/minecraft.js (92%) create mode 100644 games/minecraftping.js rename {protocols => games}/mutantfactions.js (92%) rename {protocols => games/protocols}/core.js (96%) rename {protocols => games/protocols}/gamespy3.js (69%) rename {protocols => games/protocols}/nadeo.js (100%) rename {protocols => games/protocols}/quake2.js (100%) create mode 100644 games/protocols/quake3.js rename {protocols => games/protocols}/unreal2.js (58%) rename protocols/source.js => games/protocols/valve.js (77%) create mode 100644 games/protocols/valvegold.js rename protocols/tshock.js => games/terraria.js (94%) create mode 100644 games/ut2004.js rename {protocols => games}/ut3.js (95%) rename {protocols => games}/warsow.js (77%) rename Class.js => lib/Class.js (100%) rename index.js => lib/index.js (78%) rename reader.js => lib/reader.js (69%) create mode 100644 lib/typeresolver.js delete mode 100644 protocols/killingfloor.js delete mode 100644 protocols/quake3.js delete mode 100644 protocols/ut2004.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..07e6e47 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/node_modules diff --git a/README.md b/README.md index d1ffbf4..a3b5021 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ node-GameDig - Game Server Query Library --- -Usage +Usage from Node.js --- ```shell @@ -24,7 +24,7 @@ Gamedig.query( ### Input Parameters -* **type**: One of the types from the protocols folder +* **type**: One of the game IDs listed in the game list below * **host** * **port**: (optional) Uses the protocol default if not set * **notes**: (optional) Passed through to output @@ -59,45 +59,84 @@ Some servers may return an additional player count number, which may be present Supported Games --- -###Armagetron - -###Gamespy 3 Protocol -* Minecraft -* Unreal Tournament 3 - -###GoldSrc Engine -* Half Life: Death Match -* Ricochet -* Counter-Strike: 1.6 -* \+ others - -###Nadeo Protocol +* Alien Swarm (alienswarm) +* Armagetron (armagetron) +* Build and Shoot (buildandshoot) +* Counter-Strike 1.6 (cs16) +* Counter-Strike: Source (css) +* Counter-Strike: Global Offensive (csgo) +* Dino D-Day (dinodday) +* Garry's Mod (garrysmod) +* The Hidden: Source (hidden) +* Just Cause Multiplayer (jcmp) +* Killing Floor (killingfloor) +* KzMod (kzmod) +* Left 4 Dead (left4dead) +* Left 4 Dead 2 (left4dead2) +* Minecraft (minecraft) +``` +Some minecraft servers may not respond to a typical status query. If this is the case, try using the +'minecraftping' server type instead, which uses a less accurate but more reliable solution. +``` +* Mutant Factions (mutantfactions) +* Natural Selection (ns) +* Natural Selection 2 (ns2) +* No More Room in Hell (nmrih) +* Nuclear Dawn (nucleardawn) +* Quake 2 (quake2) +* Quake 3 (quake3) +* Ricochet (ricochet) +* Rust (rust) +* The Ship (ship) +* ShootMania (shootmania) ``` Requires additional parameters: login, password ``` -* Trackmania Forever -* Trackmania 2 -* Shootmania - -###Quake 2 Protocol -* Quake 2 - -###Quake 3 Protocol -* Quake 3 Arena -* Quake 3 Team Arena -* Warsow - -###Source Engine -* Counter-Strike: Source -* Counter-Strike: Global Offensive -* Team Fortress 2 -* \+ others - -###Terraria (tshock) +* Starbound (starbound) +* Suicide Survival (suicidesurvival) +* Sven Coop (svencoop) +* Synergy (synergy) +* Team Fortress 2 (tf2) +* Terraria (terraria) ``` -Requires additional parameter: token +Requires tshock server mod, and an additional parameter: token +``` +* TrackMania 2 (trackmania2) +``` +Requires additional parameters: login, password +``` +* TrackMania Forever (trackmaniaforever) +``` +Requires additional parameters: login, password +``` +* Unreal Tournament 2004 (ut2004) +* Unreal Tournament 3 (ut3) +* Warsow (warsow) + +Don't see your game listed here? +1. Let us know so we can fix it +2. You can try using some common query protocols directly by using one of these server types: +* protocol-gamespy3 +* protocol-nadeo +* protocol-quake2 +* protocol-quake3 +* protocol-unreal2 +* protocol-valve +* protocol-valvegold + +Usage from Command Line +--- + +Want to integrate server queries from a batch script or other programming language? +You'll still need npm to install gamedig: +```shell +npm install gamedig -g ``` -###Unreal 2 Protocol -* Killing Floor -* Unreal Tournament 2004 +After installing gamedig globally, you can call gamedig via the command line +using the same parameters mentioned in the API above: +```shell +gamedig --type minecraft --host mc.example.com --port 11234 +``` + +The output of the command will be in JSON format. diff --git a/bin/gamedig.js b/bin/gamedig.js new file mode 100644 index 0000000..1a06c19 --- /dev/null +++ b/bin/gamedig.js @@ -0,0 +1,27 @@ +#!/usr/bin/env node + +var argv = require('optimist').argv; + +var debug = argv.debug; +delete argv.debug; + +var options = {}; +for(var key in argv) { + var value = argv[key]; + if( + key == '_' + || key.charAt(0) == '$' + || (typeof value != 'string' && typeof value != 'number') + ) + continue; + options[key] = value; +} + +var Gamedig = require('../lib/index'); +if(debug) Gamedig.debug = true; +Gamedig.query( + options, + function(state) { + console.log(state); + } +); diff --git a/games/aliases.txt b/games/aliases.txt new file mode 100644 index 0000000..9ef52ae --- /dev/null +++ b/games/aliases.txt @@ -0,0 +1,29 @@ +# id | pretty | protocol | port? + +alienswarm|Alien Swarm|valve +csgo|Counter-Strike: Global Offensive|valve +css|Counter-Strike: Source|valve +cs16|Counter-Strike 1.6|valvegold +dinodday|Dino D-Day|valve +garrysmod|Garry's Mod|valve +hidden|The Hidden: Source|valve +kzmod|KzMod|valve +left4dead|Left 4 Dead|valve +left4dead2|Left 4 Dead 2|valve +nmrih|No More Room in Hell|valve +ns|Natural Selection|valvegold +ns2|Natural Selection 2|valve|27016 +nucleardawn|Nuclear Dawn|valve +quake2|Quake 2|quake2 +quake3|Quake 3|quake3 +ricochet|Ricochet|valvegold +rust|Rust|valve|28016 +ship|The Ship|valve +shootmania|Shootmania|nadeo +starbound|Starbound|valve +suicidesurvival|Suicide Survival|valve +svencoop|Sven Coop|valvegold +synergy|Synergy|valve +tf2|Team Fortress 2|valve +trackmania2|Trackmania 2|nadeo +trackmaniaforever|Trackmania Forever|nadeo diff --git a/protocols/armagetron.js b/games/armagetron.js similarity index 80% rename from protocols/armagetron.js rename to games/armagetron.js index d678c3c..ac67717 100644 --- a/protocols/armagetron.js +++ b/games/armagetron.js @@ -1,4 +1,4 @@ -module.exports = require('./core').extend({ +module.exports = require('./protocols/core').extend({ init: function() { this._super(); this.pretty = 'Armagetron'; @@ -18,7 +18,7 @@ module.exports = require('./core').extend({ state.raw.port = self.readUInt(reader); state.raw.hostname = self.readString(reader,buffer); - state.name = self.readString(reader,buffer); + state.name = self.stripColorCodes(self.readString(reader,buffer)); state.raw.numplayers = self.readUInt(reader); state.raw.versionmin = self.readUInt(reader); state.raw.versionmax = self.readUInt(reader); @@ -29,10 +29,12 @@ module.exports = require('./core').extend({ var list = players.split('\n'); for(var i = 0; i < list.length; i++) { if(!list[i]) continue; - state.players.push({name:list[i]}); + state.players.push({ + name:self.stripColorCodes(list[i]) + }); } - state.raw.options = self.readString(reader,buffer); + state.raw.options = self.stripColorCodes(self.readString(reader,buffer)); state.raw.uri = self.readString(reader,buffer); state.raw.globalids = self.readString(reader,buffer); self.finish(state); @@ -56,7 +58,9 @@ module.exports = require('./core').extend({ if(i+2= 1) { + var line = addresses[0]; + self.options.port = line.port; + var srvhost = line.name; + + if(srvhost.match(/\d+\.\d+\.\d+\.\d+/)) { + self.options.address = srvhost; + c(); + } else { + // resolve yet again + fallback(srvhost); + } + return; + } + return fallback(host); + }); + }, + reset: function() { + this._super(); + if(this.socket) { + this.socket.destroy(); + delete this.socket; + } + }, + run: function(state) { + var self = this; + + var socket = this.socket = net.connect( + this.options.port, + this.options.address, + function() { + + var portBuf = new Buffer(2); + portBuf.writeUInt16BE(self.options.port,0); + + var addressBuf = new Buffer(self.options.address,'utf8'); + + var bufs = [ + varIntBuffer(4), + varIntBuffer(addressBuf.length), + addressBuf, + portBuf, + varIntBuffer(1) + ]; + self.sendPacket(0,Buffer.concat(bufs)); + self.sendPacket(0); + }); + socket.setTimeout(10000); + socket.setNoDelay(true); + + var received = new Buffer(0); + var expectedBytes = 0; + socket.on('data', function(data) { + received = Buffer.concat([received,data]); + if(expectedBytes) { + if(received.length >= expectedBytes) { + self.allReceived(received,state); + } + } else if(received.length > 10) { + expectedBytes = varint.decode(received); + received = received.slice(varint.decode.bytesRead); + } + }); + }, + sendPacket: function(id,data) { + if(!data) data = new Buffer(0); + var idBuffer = varIntBuffer(id); + var out = Buffer.concat([ + varIntBuffer(data.length+idBuffer.length), + idBuffer, + data + ]); + this.socket.write(out); + }, + allReceived: function(received,state) { + var packetId = varint.decode(received); + received = received.slice(varint.decode.bytesRead); + + var strLen = varint.decode(received); + received = received.slice(varint.decode.bytesRead); + + var str = received.toString('utf8'); + var json; + try { + json = JSON.parse(str); + delete json.favicon; + } catch(e) { + return this.fatal('Invalid JSON'); + } + + state.raw.version = json.version.name; + state.maxplayers = json.players.max; + state.raw.description = json.description.text; + for(var i = 0; i < json.players.sample.length; i++) { + state.players.push({ + id: json.players.sample[i].id, + name: json.players.sample[i].name + }); + } + while(state.players.length < json.players.online) { + state.players.push({}); + } + + this.finish(state); + } +}); diff --git a/protocols/mutantfactions.js b/games/mutantfactions.js similarity index 92% rename from protocols/mutantfactions.js rename to games/mutantfactions.js index b7a191b..34696c3 100644 --- a/protocols/mutantfactions.js +++ b/games/mutantfactions.js @@ -1,6 +1,6 @@ var request = require('request'); -module.exports = require('./core').extend({ +module.exports = require('./protocols/core').extend({ init: function() { this._super(); this.pretty = 'Mutant Factions'; @@ -22,7 +22,7 @@ module.exports = require('./core').extend({ var fields = line.split('::'); var ip = fields[2]; var port = fields[3]; - if(ip == this.options.address && port == this.options.port) { + if(ip == self.options.address && port == self.options.port) { found = fields; break; } diff --git a/protocols/core.js b/games/protocols/core.js similarity index 96% rename from protocols/core.js rename to games/protocols/core.js index 7865f86..1aa9ff6 100644 --- a/protocols/core.js +++ b/games/protocols/core.js @@ -1,8 +1,8 @@ var EventEmitter = require('events').EventEmitter, dns = require('dns'), async = require('async'), - Class = require('../Class'), - Reader = require('../reader'); + Class = require('../../lib/Class'), + Reader = require('../../lib/reader'); module.exports = Class.extend(EventEmitter,{ init: function() { @@ -154,6 +154,8 @@ module.exports = Class.extend(EventEmitter,{ if(!('address' in this.options)) return this.fatal('Attempted to send without setting an address'); if(typeof buffer == 'string') buffer = new Buffer(buffer,'binary'); + + if(this.debug) console.log("Sent",buffer,this.options.address,this.options.port); this.udpSocket.send(buffer,0,buffer.length,this.options.port,this.options.address); }, _udpResponse: function(buffer) { diff --git a/protocols/gamespy3.js b/games/protocols/gamespy3.js similarity index 69% rename from protocols/gamespy3.js rename to games/protocols/gamespy3.js index c1abbaf..c2875f9 100644 --- a/protocols/gamespy3.js +++ b/games/protocols/gamespy3.js @@ -21,28 +21,48 @@ module.exports = require('./core').extend({ var key = reader.string(); if(!key) break; var value = reader.string(); + + // reread the next line if we hit the weird ut3 bug + if(value == 'p1073741829') value = reader.string(); + state.raw[key] = value; } - - var mode = ''; + while(!reader.done()) { var mode = reader.string(); + if(mode.charCodeAt(0) <= 2) mode = mode.substring(1); + if(!mode) continue; + var offset = 0; reader.skip(1); - + while(!reader.done()) { var item = reader.string(); if(!item) break; - - if(mode.substr(-1) == '_') { - // players - state.players.push({name:item}) + + if( + mode == 'player_' + || mode == 'score_' + || mode == 'ping_' + || mode == 'team_' + || mode == 'deaths_' + || mode == 'pid_' + ) { + if(state.players.length <= offset) + state.players.push({}); } + if(mode == 'player_') state.players[offset].name = item; + if(mode == 'score_') state.players[offset].score = item; + if(mode == 'ping_') state.players[offset].ping = item; + if(mode == 'team_') state.players[offset].team = item; + if(mode == 'deaths_') state.players[offset].deaths = item; + if(mode == 'pid_') state.players[offset].pid = item; + offset++; } } if('hostname' in state.raw) state.name = state.raw.hostname; if('map' in state.raw) state.map = state.raw.map; - if('maxplayers' in state.raw) state.maxplayers = state.raw.maxplayers; + if('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers); self.finish(state); @@ -79,7 +99,7 @@ module.exports = require('./core').extend({ var id = buffer.readUInt16LE(14); var last = (id & 0x80); id = id & 0x7f; - if(last) numPackets = id+1; + if(last || self._singlePacketSplits) numPackets = id+1; packets[id] = buffer.slice(16); diff --git a/protocols/nadeo.js b/games/protocols/nadeo.js similarity index 100% rename from protocols/nadeo.js rename to games/protocols/nadeo.js diff --git a/protocols/quake2.js b/games/protocols/quake2.js similarity index 100% rename from protocols/quake2.js rename to games/protocols/quake2.js diff --git a/games/protocols/quake3.js b/games/protocols/quake3.js new file mode 100644 index 0000000..c82d602 --- /dev/null +++ b/games/protocols/quake3.js @@ -0,0 +1,21 @@ +module.exports = require('./quake2').extend({ + init: function() { + this._super(); + this.pretty = 'Quake 3'; + this.options.port = 27960; + this.sendHeader = 'getstatus'; + this.responseHeader = 'statusResponse'; + }, + finalizeState: function(state) { + state.name = this.stripColors(state.name); + for(var i in state.raw) { + state.raw[i] = this.stripColors(state.raw[i]); + } + for(var i = 0; i < state.players.length; i++) { + state.players[i].name = this.stripColors(state.players[i].name); + } + }, + stripColors: function(str) { + return str.replace(/\^(X.{6}|.)/g,''); + } +}); diff --git a/protocols/unreal2.js b/games/protocols/unreal2.js similarity index 58% rename from protocols/unreal2.js rename to games/protocols/unreal2.js index 3a7510e..807fa89 100644 --- a/protocols/unreal2.js +++ b/games/protocols/unreal2.js @@ -14,15 +14,15 @@ module.exports = require('./core').extend({ self.sendPacket(0,true,function(b) { var reader = self.reader(b); state.raw.serverid = reader.uint(4); - state.raw.ip = reader.pascal(); + state.raw.ip = self.readUnrealString(reader); state.raw.port = reader.uint(4); state.raw.queryport = reader.uint(4); - state.name = reader.pascal(); - state.map = reader.pascal(); - state.raw.gametype = reader.pascal(); - state.raw.numplayers = reader.uint(4); - state.maxplayers = reader.uint(4); - state.raw.ping = reader.uint(4); + state.name = self.readUnrealString(reader,true); + self.readUnrealString(reader); // unknown? + state.map = self.readUnrealString(reader,true); + state.raw.gametype = self.readUnrealString(reader,true); + self.readExtraInfo(reader,state); + c(); }); }, @@ -32,8 +32,8 @@ module.exports = require('./core').extend({ state.raw.mutators = []; state.raw.rules = {}; while(!reader.done()) { - var key = reader.pascal(); - var value = reader.pascal(); + var key = self.readUnrealString(reader,true); + var value = self.readUnrealString(reader,true); if(key == 'Mutator') state.raw.mutators.push(value); else state.raw.rules[key] = value; } @@ -49,7 +49,7 @@ module.exports = require('./core').extend({ var reader = self.reader(b); while(!reader.done()) { var id = reader.uint(4); - var name = reader.pascal(); + var name = self.readUnrealString(reader,true); var ping = reader.uint(4); var score = reader.uint(4); reader.skip(4); @@ -65,6 +65,34 @@ module.exports = require('./core').extend({ } ]); }, + readExtraInfo: function(reader,state) { + if(this.debug) { + console.log("UNREAL2 EXTRA INFO:"); + console.log(reader.uint(4)); + console.log(reader.uint(4)); + console.log(reader.uint(4)); + console.log(reader.uint(4)); + console.log(reader.buffer.slice(reader.i)); + } + }, + readUnrealString: function(reader, stripColor) { + var length = reader.uint(1); + var out; + if(length < 0x80) { + out = reader.string({length:length}); + } else { + length = (length&0x7f)*2; + out = length+reader.string({encoding:'ucs2',length:length}); + } + + if(out.charCodeAt(out.length-1) == 0) + out = out.substring(0,out.length-1); + + if(stripColor) + out = out.replace(/\x1b...|[\x00-\x1a]/g,''); + + return out; + }, sendPacket: function(type,required,callback) { var outbuffer = new Buffer([0x79,0,0,0,type]); diff --git a/protocols/source.js b/games/protocols/valve.js similarity index 77% rename from protocols/source.js rename to games/protocols/valve.js index dcabe6c..42787ed 100644 --- a/protocols/source.js +++ b/games/protocols/valve.js @@ -6,6 +6,10 @@ module.exports = require('./core').extend({ this._super(); this.goldsrc = false; this.options.port = 27015; + + // 2006 engines don't pass packet switching size in split packet header + // while all others do + this._skipSizeInSplitHeader = false; }, run: function(state) { @@ -34,9 +38,14 @@ module.exports = require('./core').extend({ if(self.goldsrc) state.raw.protocol = reader.uint(1); else state.raw.numbots = reader.uint(1); - state.raw.listentype = String.fromCharCode(reader.uint(1)); - state.raw.environment = String.fromCharCode(reader.uint(1)); - state.password = reader.uint(1); + state.raw.listentype = reader.uint(1); + state.raw.environment = reader.uint(1); + if(!self.goldsrc) { + state.raw.listentype = String.fromCharCode(state.raw.listentype); + state.raw.environment = String.fromCharCode(state.raw.environment); + } + + state.password = !!reader.uint(1); if(self.goldsrc) { state.raw.ismod = reader.uint(1); if(state.raw.ismod) { @@ -71,12 +80,16 @@ module.exports = require('./core').extend({ if(extraFlag & 0x01) state.raw.gameid = reader.uint(8); } + if(state.raw.protocol == 7 && state.raw.steamappid == 215) { + self._skipSizeInSplitHeader = true; + } + c(); } ); }, function(c) { - self.sendPacket(0x55,0xffffffff,false,0x41,function(b) { + self.sendPacket(self.goldsrc?0x56:0x55,0xffffffff,false,0x41,function(b) { var reader = self.reader(b); challenge = reader.uint(4); c(); @@ -89,7 +102,7 @@ module.exports = require('./core').extend({ for(var i = 0; i < num; i++) { reader.skip(1); var name = reader.string(); - var score = reader.uint(4); + var score = reader.int(4); var time = reader.float(); // connecting players don't could as players. @@ -131,6 +144,12 @@ module.exports = require('./core').extend({ state.raw.rules[key] = value; } c(); + }, function() { + // no rules were returned after timeout -- + // the server probably has them disabled + // ignore the timeout + c(); + return true; }); }, function(c) { @@ -138,7 +157,7 @@ module.exports = require('./core').extend({ } ]); }, - sendPacket: function(type,challenge,payload,expect,callback) { + sendPacket: function(type,challenge,payload,expect,callback,ontimeout) { var self = this; var challengeLength = challenge === false ? 0 : 4; @@ -152,6 +171,7 @@ module.exports = require('./core').extend({ function received(payload) { var type = payload.readUInt8(0); + if(self.debug) console.log("Received "+type+" expected "+expect); if(type != expect) return; callback(payload.slice(1)); return true; @@ -175,16 +195,22 @@ module.exports = require('./core').extend({ if(self.goldsrc) { id = buffer.readUInt8(8); numPackets = id & 0x0f; - id = id & 0xf0 >> 4; + id = (id & 0xf0) >> 4; payload = buffer.slice(9); } else { numPackets = buffer.readUInt8(8); id = buffer.readUInt8(9); - if(id == 0 && bzip) payload = buffer.slice(20); - else payload = buffer.slice(12); + var sizeOffset = self._skipSizeInSplitHeader ? 0 : 2; + if(id == 0 && bzip) payload = buffer.slice(18+sizeOffset); + else payload = buffer.slice(10+sizeOffset); } packets[id] = payload; + + if(self.debug) { + console.log("Received partial packet id: "+id); + console.log("Expecting "+numPackets+" packets, have "+Object.keys(packets).length); + } if(!numPackets || Object.keys(packets).length != numPackets) return; @@ -198,11 +224,10 @@ module.exports = require('./core').extend({ list.push(packets[i]); } var assembled = Buffer.concat(list); - var payload = assembled.slice(4); - if(bzip) payload = Bzip2.uncompressFile(payload); + if(bzip) assembled = new Buffer(Bzip2.decompressFile(assembled)); - return received(payload); + return received(assembled.slice(4)); } - }); + },ontimeout); } }); diff --git a/games/protocols/valvegold.js b/games/protocols/valvegold.js new file mode 100644 index 0000000..5177bc0 --- /dev/null +++ b/games/protocols/valvegold.js @@ -0,0 +1,6 @@ +module.exports = require('./valve').extend({ + init: function() { + this._super(); + this.goldsrc = true; + } +}); diff --git a/protocols/tshock.js b/games/terraria.js similarity index 94% rename from protocols/tshock.js rename to games/terraria.js index 678f8d8..f2b581c 100644 --- a/protocols/tshock.js +++ b/games/terraria.js @@ -1,6 +1,6 @@ var request = require('request'); -module.exports = require('./core').extend({ +module.exports = require('./protocols/core').extend({ init: function() { this._super(); this.pretty = 'Terraria'; diff --git a/games/ut2004.js b/games/ut2004.js new file mode 100644 index 0000000..06f883d --- /dev/null +++ b/games/ut2004.js @@ -0,0 +1,12 @@ +module.exports = require('./protocols/unreal2').extend({ + init: function() { + this._super(); + this.options.port = 7778; + this.pretty = 'Unreal Tournament 2004'; + }, + readExtraInfo: function(reader,state) { + reader.skip(18); + state.raw.numplayers = reader.uint(4); + state.maxplayers = reader.uint(4); + } +}); diff --git a/protocols/ut3.js b/games/ut3.js similarity index 95% rename from protocols/ut3.js rename to games/ut3.js index d5b4b3e..d5de6b7 100644 --- a/protocols/ut3.js +++ b/games/ut3.js @@ -1,4 +1,4 @@ -module.exports = require('./gamespy3').extend({ +module.exports = require('./protocols/gamespy3').extend({ init: function() { this._super(); this.pretty = 'Unreal Tournament 3'; diff --git a/protocols/warsow.js b/games/warsow.js similarity index 77% rename from protocols/warsow.js rename to games/warsow.js index ef9e258..e007017 100644 --- a/protocols/warsow.js +++ b/games/warsow.js @@ -1,10 +1,10 @@ -module.exports = require('./quake3').extend({ +module.exports = require('./protocols/quake3').extend({ init: function() { this._super(); this.pretty = 'Warsow'; this.options.port = 44400; }, - prepState: function(state) { + finalizeState: function(state) { this._super(state); if(state.players) { for(var i = 0; i < state.players.length; i++) { diff --git a/Class.js b/lib/Class.js similarity index 100% rename from Class.js rename to lib/Class.js diff --git a/index.js b/lib/index.js similarity index 78% rename from index.js rename to lib/index.js index a705dac..5619340 100644 --- a/index.js +++ b/lib/index.js @@ -1,7 +1,8 @@ var dgram = require('dgram'), EventEmitter = require('events').EventEmitter, util = require('util'), - dns = require('dns'); + dns = require('dns'), + TypeResolver = require('./typeresolver'); var activeQueries = []; @@ -27,12 +28,16 @@ Gamedig = { query: function(options,callback) { if(callback) options.callback = callback; - var type = (options.type || '').replace(/\W/g,''); - var protocol = require('./protocols/'+type); - - var query = new protocol(); + var query = TypeResolver(options.type); + if(!query) { + process.nextTick(function() { + callback({error:'Invalid server type: '+options.type}); + }); + return; + } + query.debug = Gamedig.debug; query.udpSocket = udpSocket; - query.type = type; + query.type = options.type; // copy over options for(var i in options) query.options[i] = options[i]; diff --git a/reader.js b/lib/reader.js similarity index 69% rename from reader.js rename to lib/reader.js index fe9dacc..f95569a 100644 --- a/reader.js +++ b/lib/reader.js @@ -1,5 +1,16 @@ var Iconv = require('iconv-lite'), - Bignum = require('bignum'); + Long = require('long'); + +function readUInt64BE(buffer,offset) { + var high = buffer.readUInt32BE(offset); + var low = buffer.readUInt32BE(offset+4); + return new Long(low,high,true); +} +function readUInt64LE(buffer,offset) { + var low = buffer.readUInt32LE(offset); + var high = buffer.readUInt32LE(offset+4); + return new Long(low,high,true); +} function Reader(query,buffer) { this.query = query; @@ -40,7 +51,6 @@ Reader.prototype = { end = start+options.length; if(end > this.buffer.length) return ''; this.i = end; - if(options.stripnull && this.buffer.readUInt8(end-1) == 0) end--; } var out = this.buffer.slice(start, end); @@ -52,6 +62,22 @@ Reader.prototype = { } return out; }, + int: function(bytes) { + var r = 0; + if(this.i+bytes <= this.buffer.length) { + if(this.query.byteorder == 'be') { + if(bytes == 1) r = this.buffer.readInt8(this.i); + else if(bytes == 2) r = this.buffer.readInt16BE(this.i); + else if(bytes == 4) r = this.buffer.readInt32BE(this.i); + } else { + if(bytes == 1) r = this.buffer.readInt8(this.i); + else if(bytes == 2) r = this.buffer.readInt16LE(this.i); + else if(bytes == 4) r = this.buffer.readInt32LE(this.i); + } + } + this.i += bytes; + return r; + }, uint: function(bytes) { var r = 0; if(this.i+bytes <= this.buffer.length) { @@ -59,12 +85,12 @@ Reader.prototype = { if(bytes == 1) r = this.buffer.readUInt8(this.i); else if(bytes == 2) r = this.buffer.readUInt16BE(this.i); else if(bytes == 4) r = this.buffer.readUInt32BE(this.i); - else if(bytes == 8) r = Bignum.fromBuffer(this.buffer.slice(this.i,this.i+8),{endian:'big',size:'auto'}); + else if(bytes == 8) r = readUInt64BE(this.buffer,this.i).toString(); } else { if(bytes == 1) r = this.buffer.readUInt8(this.i); else if(bytes == 2) r = this.buffer.readUInt16LE(this.i); else if(bytes == 4) r = this.buffer.readUInt32LE(this.i); - else if(bytes == 8) r = Bignum.fromBuffer(this.buffer.slice(this.i,this.i+8),{endian:'little',size:'auto'}); + else if(bytes == 8) r = readUInt64LE(this.buffer,this.i).toString(); } } this.i += bytes; @@ -79,16 +105,6 @@ Reader.prototype = { this.i += 4; return r; }, - pascal: function(enc) { - if(this.i >= this.buffer.length) return ''; - var length = this.buffer.readUInt8(this.i); - this.i++; - return this.string({ - encoding: enc, - length: length, - stripnull: true - }); - }, done: function() { return this.i >= this.buffer.length; } diff --git a/lib/typeresolver.js b/lib/typeresolver.js new file mode 100644 index 0000000..bbd3b98 --- /dev/null +++ b/lib/typeresolver.js @@ -0,0 +1,51 @@ +var Path = require('path'), + fs = require('fs'); + +var gamesDir = Path.normalize(__dirname+'/../games'); + +function readAliases() { + var lines = fs.readFileSync(gamesDir+'/aliases.txt','utf8').split('\n'); + var aliases = {}; + + lines.forEach(function(line) { + line = line.trim(); + if(!line) return; + if(line.charAt(0) == '#') return; + var split = line.split('|'); + + aliases[split[0].trim()] = { + pretty: split[1].trim(), + protocol: split[2].trim(), + port: split[3] ? parseInt(split[3]) : 0 + }; + }); + return aliases; +} +var aliases = readAliases(); + +function createQueryInstance(type) { + type = Path.basename(type); + + var path = gamesDir+'/'+type; + if(type.substr(0,9) == 'protocol-') { + path = gamesDir+'/protocols/'+type.substr(9); + } + + if(!fs.existsSync(path+'.js')) return false; + var protocol = require(path); + + return new protocol(); +} + +module.exports = function(type) { + var alias = aliases[type]; + + if(alias) { + var query = createQueryInstance('protocol-'+alias.protocol); + if(!query) return false; + query.pretty = alias.pretty; + if(alias.port) query.options.port = alias.port; + return query; + } + return createQueryInstance(type); +} diff --git a/package.json b/package.json index e070634..38b3ece 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "util", "server" ], - "main": "index.js", + "main": "lib/index.js", "author": "Michael Morrison", "version": "0.1.2", "repository" : { @@ -26,11 +26,16 @@ } ], "dependencies": { - "iconv-lite": ">=0.2.10", - "bignum": ">=0.6.1", - "async": ">=0.2.9", - "compressjs": ">=1.0.0", - "gbxremote": "git://github.com/sonicsnes/node-gbxremote.git", - "request": ">=2.22.0" + "iconv-lite": "~0.2.11", + "long": "~1.1.2", + "async": "~0.2.10", + "compressjs": "~1.0.1", + "gbxremote": "~0.1.4", + "request": "~2.33.0", + "optimist": "~0.6.0", + "varint": "~1.0.0" + }, + "bin": { + "gamedig": "bin/gamedig.js" } } diff --git a/protocols/killingfloor.js b/protocols/killingfloor.js deleted file mode 100644 index 41c77e9..0000000 --- a/protocols/killingfloor.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = require('./unreal2').extend({ - init: function() { - this._super(); - this.pretty = 'Killing Floor'; - this.options.port = 7708; - } -}); diff --git a/protocols/quake3.js b/protocols/quake3.js deleted file mode 100644 index cc033ae..0000000 --- a/protocols/quake3.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = require('./quake2').extend({ - init: function() { - this._super(); - this.pretty = 'Quake 3'; - this.options.port = 27960; - this.sendHeader = 'getstatus'; - this.responseHeader = 'statusResponse'; - } -}); diff --git a/protocols/ut2004.js b/protocols/ut2004.js deleted file mode 100644 index cddbbd6..0000000 --- a/protocols/ut2004.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = require('./unreal2').extend({ - init: function() { - this._super(); - this.pretty = 'Unreal Tournament 2004'; - this.options.port = 7778; - } -});