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; - } -});