diff --git a/README.md b/README.md index 150fe04..37feda2 100644 --- a/README.md +++ b/README.md @@ -22,25 +22,34 @@ Gamedig.query( ); ``` -State Object +Callback Function --- -The callback function is "guaranteed" to be called exactly once, indicating either a query error or timeout -(in the state's error key), or with a state object containg the successful results. +The callback function is "guaranteed" to be called exactly once. -The returned state object may contain some or all of the following keys: +If an error occurs, the returned object will contain an "error" key, indicating the issue. +If the error key exists, it should be assumed that the game server is offline or unreachable. + +Otherwise, the returned object will contain the following keys: + +Guaranteed: -* error * name -* numplayers -* maxplayers -* players - * name - * ping - * score * map -* gametype +* password (boolean) +* maxplayers +* players (may contain name, ping, score, team, address) +* bots (same as players) +* raw (contains special keys depending on the type of server queried - UNSTABLE) +* notes (passed through from the input) +* query (details about the query performed) + * host + * address + * port + * type + * pretty (a "pretty" string describing the game) -Many other keys will also be available will be available on a game by game basis. +It can usually be assumed that the number of players online is equal to the length of the players array. +Some servers may return an additional player count number, which may be present in the unstable raw object. Supported Games --- diff --git a/protocols/armagetron.js b/protocols/armagetron.js index 59415fd..d678c3c 100644 --- a/protocols/armagetron.js +++ b/protocols/armagetron.js @@ -1,41 +1,40 @@ module.exports = require('./core').extend({ init: function() { this._super(); + this.pretty = 'Armagetron'; this.encoding = 'latin1'; this.byteorder = 'be'; this.options.port = 4534; }, - run: function() { + run: function(state) { var self = this; var b = new Buffer([0,0x35,0,0,0,0,0,0x11]); this.udpSend(b,function(buffer) { - var state = {}; var reader = self.reader(buffer); reader.skip(6); - state.port = self.readUInt(reader); - state.hostname = self.readString(reader,buffer); + state.raw.port = self.readUInt(reader); + state.raw.hostname = self.readString(reader,buffer); state.name = self.readString(reader,buffer); - state.numplayers = self.readUInt(reader); - state.versionmin = self.readUInt(reader); - state.versionmax = self.readUInt(reader); - state.version = self.readString(reader,buffer); + state.raw.numplayers = self.readUInt(reader); + state.raw.versionmin = self.readUInt(reader); + state.raw.versionmax = self.readUInt(reader); + state.raw.version = self.readString(reader,buffer); state.maxplayers = self.readUInt(reader); var players = self.readString(reader,buffer); var list = players.split('\n'); - state.players = []; for(var i = 0; i < list.length; i++) { if(!list[i]) continue; state.players.push({name:list[i]}); } - state.options = self.readString(reader,buffer); - state.uri = self.readString(reader,buffer); - state.globalids = self.readString(reader,buffer); + state.raw.options = self.readString(reader,buffer); + state.raw.uri = self.readString(reader,buffer); + state.raw.globalids = self.readString(reader,buffer); self.finish(state); return true; }); diff --git a/protocols/core.js b/protocols/core.js index 467fc5d..df987f3 100644 --- a/protocols/core.js +++ b/protocols/core.js @@ -33,21 +33,40 @@ module.exports = Class.extend(EventEmitter,{ this.done({error: err.toString()}); }, - prepState: function(state) { + initState: function() { + return { + name: '', + map: '', + password: false, + + raw: {}, + + maxplayers: 0, + players: [], + bots: [] + }; + }, + finalizeState: function(state) { if(this.options.notes) state.notes = this.options.notes; - if('host' in this.options) state.queryhost = this.options.host; - if('port' in this.options) state.queryport = this.options.port; + + state.query = {}; + if('host' in this.options) state.query.host = this.options.host; + if('port' in this.options) state.query.port = this.options.port; + state.query.type = this.type; + if('pretty' in this) state.query.pretty = this.pretty; + + if('players' in state) state.numplayers = state.players.length; + if('bots' in state) state.numbots = state.bots.length; }, finish: function(result) { + this.finalizeState(result); this.done(result); }, done: function(result) { if(this.finished) return; clearTimeout(this.globalTimeoutTimer); - this.prepState(result); - this.reset(); this.finished = true; this.emit('finished',result); @@ -80,7 +99,7 @@ module.exports = Class.extend(EventEmitter,{ self.parseDns(self.options.host,c); } }, function(c) { - self.run(); + self.run(self.initState()); } ]); @@ -98,12 +117,12 @@ module.exports = Class.extend(EventEmitter,{ reader: function(buffer) { return new Reader(this,buffer); }, - translateState: function(state,trans) { + translate: function(obj,trans) { for(var from in trans) { var to = trans[from]; - if(from in state) { - if(to) state[to] = state[from]; - delete state[from]; + if(from in obj) { + if(to) obj[to] = obj[from]; + delete obj[from]; } } }, diff --git a/protocols/gamespy3.js b/protocols/gamespy3.js index 4ce6121..7976893 100644 --- a/protocols/gamespy3.js +++ b/protocols/gamespy3.js @@ -5,7 +5,7 @@ module.exports = require('./core').extend({ this.encoding = 'latin1'; this.byteorder = 'be'; }, - run: function() { + run: function(state) { var self = this; this.sendPacket(9,false,false,false,function(buffer) { @@ -16,15 +16,12 @@ module.exports = require('./core').extend({ self.sendPacket(0,challenge,new Buffer([0xff,0xff,0xff,0x01]),true,function(buffer) { var reader = self.reader(buffer); - var state = { - players:[] - }; while(!reader.done()) { var key = reader.string(); if(!key) break; var value = reader.string(); - state[key] = value; + state.raw[key] = value; } var mode = ''; @@ -42,6 +39,9 @@ module.exports = require('./core').extend({ } } } + + if('hostname' in state.raw) state.name = state.raw.hostname; + if('map' in state.raw) state.map = state.raw.map; self.finish(state); diff --git a/protocols/killingfloor.js b/protocols/killingfloor.js index e1e69ce..41c77e9 100644 --- a/protocols/killingfloor.js +++ b/protocols/killingfloor.js @@ -1,6 +1,7 @@ module.exports = require('./unreal2').extend({ init: function() { this._super(); + this.pretty = 'Killing Floor'; this.options.port = 7708; } }); diff --git a/protocols/minecraft.js b/protocols/minecraft.js index 9f3030e..e6bf42f 100644 --- a/protocols/minecraft.js +++ b/protocols/minecraft.js @@ -3,6 +3,7 @@ var dns = require('dns'); module.exports = require('./gamespy3').extend({ init: function() { this._super(); + this.pretty = 'Minecraft'; this.maxAttempts = 2; this.options.port = 25565; }, diff --git a/protocols/nadeo.js b/protocols/nadeo.js index d55b585..8ffb189 100644 --- a/protocols/nadeo.js +++ b/protocols/nadeo.js @@ -14,7 +14,7 @@ module.exports = require('./core').extend({ this.gbxclient = false; } }, - run: function() { + run: function(state) { var self = this; var cmds = [ @@ -46,8 +46,6 @@ module.exports = require('./core').extend({ }); } }, function() { - var state = {}; - var gamemode = ''; var igm = results[5].GameMode; if(igm == 0) gamemode="Rounds"; @@ -61,14 +59,13 @@ module.exports = require('./core').extend({ state.password = (results[3].Password != 'No password'); state.maxplayers = results[3].CurrentMaxPlayers; state.map = self.stripColors(results[4].Name); - state.gametype = gamemode; + state.raw.gametype = gamemode; - state.players = []; results[2].forEach(function(player) { state.players.push({name:self.stripColors(player.Name)}); }); - console.log(state); + self.finish(state); }); }, stripColors: function(str) { diff --git a/protocols/quake2.js b/protocols/quake2.js index b441f80..9625243 100644 --- a/protocols/quake2.js +++ b/protocols/quake2.js @@ -1,13 +1,14 @@ module.exports = require('./core').extend({ init: function() { this._super(); + this.pretty = 'Quake 2'; this.options.port = 27910; this.encoding = 'latin1'; this.delimiter = '\n'; this.sendHeader = 'status'; this.responseHeader = 'print'; }, - run: function() { + run: function(state) { var self = this; this.udpSend('\xff\xff\xff\xff'+this.sendHeader+'\x00',function(buffer) { @@ -15,18 +16,15 @@ module.exports = require('./core').extend({ var header = reader.string(); if(header != '\xff\xff\xff\xff'+this.responseHeader) return; - var state = {}; - var info = reader.string().split('\\'); if(info[0] == '') info.shift(); while(true) { var key = info.shift(); var value = info.shift(); if(typeof value == 'undefined') break; - state[key] = value; + state.raw[key] = value; } - state.players = []; while(!reader.done()) { var player = reader.string(); @@ -50,11 +48,16 @@ module.exports = require('./core').extend({ var name = args[2] || ''; var address = args[3] || ''; - state.players.push({ + (ping == 0 ? state.bots : state.players).push({ frags:frags, ping:ping, name:name, address:address }); } + if('g_needpass' in state.raw) state.password = state.raw.g_needpass; + if('mapname' in state.raw) state.map = state.raw.mapname; + if('sv_maxclients' in state.raw) state.maxplayers = state.raw.sv_maxclients; + if('sv_hostname' in state.raw) state.name = state.raw.sv_hostname; + self.finish(state); return true; }); diff --git a/protocols/quake3.js b/protocols/quake3.js index 4fcb237..cc033ae 100644 --- a/protocols/quake3.js +++ b/protocols/quake3.js @@ -1,6 +1,7 @@ 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/source.js b/protocols/source.js index b2710b4..fca713d 100644 --- a/protocols/source.js +++ b/protocols/source.js @@ -7,11 +7,10 @@ module.exports = require('./core').extend({ this.goldsrc = false; this.options.port = 27015; }, - run: function() { + run: function(state) { var self = this; var challenge; - var state = {}; async.series([ function(c) { @@ -21,55 +20,55 @@ module.exports = require('./core').extend({ function(b) { var reader = self.reader(b); - if(self.goldsrc) state.address = reader.string(); - else state.protocol = reader.uint(1); + if(self.goldsrc) state.raw.address = reader.string(); + else state.raw.protocol = reader.uint(1); state.name = reader.string(); state.map = reader.string(); - state.folder = reader.string(); - state.game = reader.string(); - state.steamappid = reader.uint(2); - state.numplayers = reader.uint(1); + state.raw.folder = reader.string(); + state.raw.game = reader.string(); + state.raw.steamappid = reader.uint(2); + state.raw.numplayers = reader.uint(1); state.maxplayers = reader.uint(1); - if(self.goldsrc) state.protocol = reader.uint(1); - else state.numbots = reader.uint(1); + if(self.goldsrc) state.raw.protocol = reader.uint(1); + else state.raw.numbots = reader.uint(1); - state.listentype = String.fromCharCode(reader.uint(1)); - state.environment = String.fromCharCode(reader.uint(1)); - state.passworded = reader.uint(1); + state.raw.listentype = String.fromCharCode(reader.uint(1)); + state.raw.environment = String.fromCharCode(reader.uint(1)); + state.password = reader.uint(1); if(self.goldsrc) { - state.ismod = reader.uint(1); - if(state.ismod) { - state.modlink = reader.string(); - state.moddownload = reader.string(); + state.raw.ismod = reader.uint(1); + if(state.raw.ismod) { + state.raw.modlink = reader.string(); + state.raw.moddownload = reader.string(); reader.skip(1); - state.modversion = reader.uint(4); - state.modsize = reader.uint(4); - state.modtype = reader.uint(1); - state.moddll = reader.uint(1); + state.raw.modversion = reader.uint(4); + state.raw.modsize = reader.uint(4); + state.raw.modtype = reader.uint(1); + state.raw.moddll = reader.uint(1); } } - state.secure = reader.uint(1); + state.raw.secure = reader.uint(1); if(self.goldsrc) { - state.numbots = reader.uint(1); + state.raw.numbots = reader.uint(1); } else { - if(state.folder == 'ship') { - state.shipmode = reader.uint(1); - state.shipwitnesses = reader.uint(1); - state.shipduration = reader.uint(1); + if(state.raw.folder == 'ship') { + state.raw.shipmode = reader.uint(1); + state.raw.shipwitnesses = reader.uint(1); + state.raw.shipduration = reader.uint(1); } - state.version = reader.string(); + state.raw.version = reader.string(); var extraFlag = reader.uint(1); - if(extraFlag & 0x80) state.port = reader.uint(2); - if(extraFlag & 0x10) state.steamid = reader.uint(8); + if(extraFlag & 0x80) state.raw.port = reader.uint(2); + if(extraFlag & 0x10) state.raw.steamid = reader.uint(8); if(extraFlag & 0x40) { - state.sourcetvport = reader.uint(2); - state.sourcetvname = reader.string(); + state.raw.sourcetvport = reader.uint(2); + state.raw.sourcetvname = reader.string(); } - if(extraFlag & 0x20) state.tags = reader.string(); - if(extraFlag & 0x01) state.gameid = reader.uint(8); + if(extraFlag & 0x20) state.raw.tags = reader.string(); + if(extraFlag & 0x01) state.raw.gameid = reader.uint(8); } c(); @@ -87,13 +86,12 @@ module.exports = require('./core').extend({ self.sendPacket(0x55,challenge,false,0x44,function(b) { var reader = self.reader(b); var num = reader.uint(1); - state.players = []; for(var i = 0; i < num; i++) { reader.skip(1); var name = reader.string(); var score = reader.uint(4); var time = reader.float(); - state.players.push({ + (time == -1 ? state.bots : state.players).push({ name:name, score:score, time:time }); } @@ -104,11 +102,11 @@ module.exports = require('./core').extend({ self.sendPacket(0x56,challenge,false,0x45,function(b) { var reader = self.reader(b); var num = reader.uint(2); - state.rules = []; + state.raw.rules = []; for(var i = 0; i < num; i++) { var key = reader.string(); var value = reader.string(); - state.rules[key] = value; + state.raw.rules[key] = value; } c(); }); diff --git a/protocols/tshock.js b/protocols/tshock.js index 170473d..9425088 100644 --- a/protocols/tshock.js +++ b/protocols/tshock.js @@ -3,9 +3,10 @@ var request = require('request'); module.exports = require('./core').extend({ init: function() { this._super(); + this.pretty = 'Terraria'; this.options.port = 7878; }, - run: function() { + run: function(state) { var self = this; request({ uri: 'http://'+this.options.address+':'+this.options.port+'/status', @@ -21,18 +22,16 @@ module.exports = require('./core').extend({ if(json.status != 200) return self.error('Invalid status'); - var players = []; var split = json.players.split(','); split.forEach(function(one) { - players.push({name:one}); + state.players.push({name:one}); }); + + state.name = json.name; + state.raw.port = json.port; + state.raw.numplayers = json.playercount; - self.finish({ - 'name': json.name, - 'port': json.port, - 'numplayers': json.playercount, - 'players': players - }); + self.finish(state); }); } }); diff --git a/protocols/unreal2.js b/protocols/unreal2.js index 5ccde7f..3a7510e 100644 --- a/protocols/unreal2.js +++ b/protocols/unreal2.js @@ -5,54 +5,55 @@ module.exports = require('./core').extend({ this._super(); this.encoding = 'latin1'; }, - run: function() { + run: function(state) { var self = this; - var state = {}; async.series([ function(c) { self.sendPacket(0,true,function(b) { var reader = self.reader(b); - state.serverid = reader.uint(4); - state.ip = reader.pascal(); - state.port = reader.uint(4); - state.queryport = reader.uint(4); + state.raw.serverid = reader.uint(4); + state.raw.ip = reader.pascal(); + state.raw.port = reader.uint(4); + state.raw.queryport = reader.uint(4); state.name = reader.pascal(); state.map = reader.pascal(); - state.gametype = reader.pascal(); - state.numplayers = reader.uint(4); + state.raw.gametype = reader.pascal(); + state.raw.numplayers = reader.uint(4); state.maxplayers = reader.uint(4); - state.ping = reader.uint(4); + state.raw.ping = reader.uint(4); c(); }); }, function(c) { self.sendPacket(1,true,function(b) { var reader = self.reader(b); - state.mutators = []; - state.rules = {}; + state.raw.mutators = []; + state.raw.rules = {}; while(!reader.done()) { var key = reader.pascal(); var value = reader.pascal(); - if(key == 'Mutator') state.mutators.push(value); - else state.rules[key] = value; + if(key == 'Mutator') state.raw.mutators.push(value); + else state.raw.rules[key] = value; } + + if('GamePassword' in state.raw.rules) + state.password = state.raw.rules.GamePassword != 'True'; + c(); }); }, function(c) { self.sendPacket(2,false,function(b) { var reader = self.reader(b); - state.players = []; while(!reader.done()) { var id = reader.uint(4); - console.log(b.slice(reader.offset())); var name = reader.pascal(); var ping = reader.uint(4); var score = reader.uint(4); reader.skip(4); - state.players.push({ + (ping == 0 ? state.bots : state.players).push({ id: id, name: name, ping: ping, score: score }); } diff --git a/protocols/ut2004.js b/protocols/ut2004.js index f3a4c3f..cddbbd6 100644 --- a/protocols/ut2004.js +++ b/protocols/ut2004.js @@ -1,6 +1,7 @@ module.exports = require('./unreal2').extend({ init: function() { this._super(); + this.pretty = 'Unreal Tournament 2004'; this.options.port = 7778; } }); diff --git a/protocols/ut3.js b/protocols/ut3.js index 7276a88..d5b4b3e 100644 --- a/protocols/ut3.js +++ b/protocols/ut3.js @@ -1,13 +1,13 @@ module.exports = require('./gamespy3').extend({ init: function() { this._super(); + this.pretty = 'Unreal Tournament 3'; this.options.port = 6500; }, - prepState: function(state) { + finalizeState: function(state) { this._super(state); - this.translateState(state,{ - //'OwningPlayerName': 'hostname', + this.translate(state.raw,{ 'mapname': false, 'p1073741825': 'map', 'p1073741826': 'gametype', @@ -35,7 +35,7 @@ module.exports = require('./gamespy3').extend({ 'p268435968': false, 'p268435969': false }); - + function split(a) { var s = a.split('\x1c'); s = s.filter(function(e) { return e }); @@ -43,5 +43,9 @@ module.exports = require('./gamespy3').extend({ } if('custom_mutators' in state) state['custom_mutators'] = split(state['custom_mutators']); if('stock_mutators' in state) state['stock_mutators'] = split(state['stock_mutators']); + + if('map' in state.raw) state.map = state.raw.map; + if('password' in state.raw) state.password = state.raw.password; + if('servername' in state.raw) state.name = state.raw.servername; } }); diff --git a/protocols/warsow.js b/protocols/warsow.js index b59f449..ef9e258 100644 --- a/protocols/warsow.js +++ b/protocols/warsow.js @@ -1,6 +1,7 @@ module.exports = require('./quake3').extend({ init: function() { this._super(); + this.pretty = 'Warsow'; this.options.port = 44400; }, prepState: function(state) {