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.
This commit is contained in:
Michael Morrison 2014-01-31 16:27:52 -06:00
parent a89fb7bbdf
commit c82554ad1a
31 changed files with 573 additions and 135 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/node_modules

115
README.md
View File

@ -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.

27
bin/gamedig.js Normal file
View File

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

29
games/aliases.txt Normal file
View File

@ -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

View File

@ -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<len) out += String.fromCharCode(hi);
}
out = out.replace(/0x[0-9a-f]{6}/g,''); // strip color codes
return out;
},
stripColorCodes: function(str) {
return str.replace(/0x[0-9a-f]{6}/g,'');
}
});

View File

@ -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 = 'Build and Shoot';

30
games/jcmp.js Normal file
View File

@ -0,0 +1,30 @@
/*
module.exports = require('./protocols/valve').extend({
init: function() {
this._super();
this.options.port = 7777;
this.pretty = 'Just Cause 2 Multiplayer';
}
});
*/
// supposedly, gamespy3 is the "official" query protocol for jcmp,
// but it's broken (requires singlePacketSplits), and doesn't include player names
module.exports = require('./protocols/gamespy3').extend({
init: function() {
this._super();
this.options.port = 7777;
this.pretty = 'Just Cause 2 Multiplayer';
this._singlePacketSplits = true;
},
finalizeState: function(state) {
this._super(state);
console.log(state.players.length);
console.log(state.raw.numplayers);
if(!state.players.length && parseInt(state.raw.numplayers)) {
for(var i = 0; i < parseInt(state.raw.numplayers); i++) {
state.players.push({});
}
}
}
});

13
games/killingfloor.js Normal file
View File

@ -0,0 +1,13 @@
module.exports = require('./protocols/unreal2').extend({
init: function() {
this._super();
this.options.port = 7708;
this.pretty = 'Killing Floor';
},
readExtraInfo: function(reader,state) {
state.raw.numplayers = reader.uint(4);
state.maxplayers = reader.uint(4);
state.raw.wavecurrent = reader.uint(4);
state.raw.wavetotal = reader.uint(4);
}
});

View File

@ -1,6 +1,6 @@
var dns = require('dns');
module.exports = require('./gamespy3').extend({
module.exports = require('./protocols/gamespy3').extend({
init: function() {
this._super();
this.pretty = 'Minecraft';

127
games/minecraftping.js Normal file
View File

@ -0,0 +1,127 @@
var dns = require('dns'),
net = require('net'),
varint = require('varint');
function varIntBuffer(num) {
return new Buffer(varint.encode(num));
}
module.exports = require('./protocols/core').extend({
init: function() {
this._super();
this.pretty = 'Minecraft';
this.options.port = 25565;
},
parseDns: function(host,c) {
var self = this;
var _super = this._super;
function fallback(h) { _super.call(self,h,c); }
dns.resolve('_minecraft._tcp.'+host, 'SRV', function(err,addresses) {
if(err) return fallback(host);
if(addresses.length >= 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);
}
});

View File

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

View File

@ -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) {

View File

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

21
games/protocols/quake3.js Normal file
View File

@ -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,'');
}
});

View File

@ -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]);

View File

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

View File

@ -0,0 +1,6 @@
module.exports = require('./valve').extend({
init: function() {
this._super();
this.goldsrc = true;
}
});

View File

@ -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';

12
games/ut2004.js Normal file
View File

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

View File

@ -1,4 +1,4 @@
module.exports = require('./gamespy3').extend({
module.exports = require('./protocols/gamespy3').extend({
init: function() {
this._super();
this.pretty = 'Unreal Tournament 3';

View File

@ -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++) {

View File

@ -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];

View File

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

51
lib/typeresolver.js Normal file
View File

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

View File

@ -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"
}
}

View File

@ -1,7 +0,0 @@
module.exports = require('./unreal2').extend({
init: function() {
this._super();
this.pretty = 'Killing Floor';
this.options.port = 7708;
}
});

View File

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

View File

@ -1,7 +0,0 @@
module.exports = require('./unreal2').extend({
init: function() {
this._super();
this.pretty = 'Unreal Tournament 2004';
this.options.port = 7778;
}
});