Super epic commit 2

Added pretty much every game ever
Tons of new protocols and game definitions
Cleaned up and discovered some new tricks in gamespy3 and quake2
This commit is contained in:
Michael Morrison 2014-02-01 17:46:10 -06:00
parent b51877ef5c
commit e23aa6cf9c
21 changed files with 854 additions and 222 deletions

View File

@ -61,6 +61,7 @@ Supported Games
* Age of Chivalry (ageofchivalry)
* Alien Swarm (alienswarm)
* Aliens vs Predator 2 (avp2)
* America's Army 1 (americasarmy) [[Separate Query Port - Usually port+1](#separate-query-port)]
* America's Army 2 (americasarmy2) [[Separate Query Port - Usually port+1](#separate-query-port)]
* America's Army 3 (americasarmy3) [[Separate Query Port - Usually 27020](#separate-query-port)]
@ -71,42 +72,99 @@ Supported Games
* Armagetron (armagetron)
* Battlefield 1942 (bf1942) [[Separate Query Port - Usually 23000](#separate-query-port)]
* Battlefield 2 (bf2) [[Separate Query Port - Usually 29900](#separate-query-port)]
* Battlefield 3 (bf3) [[Separate Query Port - Usually port+22000](#separate-query-port)]
* Battlefield 4 (bf4) [[Separate Query Port - Usually port+22000](#separate-query-port)]
* Battlefield: Bad Company 2 (bfbc2) [[Separate Query Port - Usually 48888](#separate-query-port)]
* Battlefield: Vietnam (bfv) [[Separate Query Port - Usually 48888](#separate-query-port)]
* Brink (brink) [[Separate Query Port - Usually port+1](#separate-query-port)]
* Build and Shoot (buildandshoot)
* Call of Duty (cod)
* Call of Duty 2 (cod2)
* Call of Duty 4 (cod4)
* Call of Duty: Modern Warfare 3 (codmw3) [[Separate Query Port - Usually port+2](#separate-query-port)]
* Call of Duty: United Offensive (coduo)
* Call of Duty: World at War (codwaw)
* Counter-Strike 1.6 (cs16)
* Counter-Strike: Condition Zero (cscz)
* Counter-Strike: Source (css)
* Counter-Strike: Global Offensive (csgo)
* Crysis (crysis)
* Crysis 2 (crysis2)
* Crysis Wars (crysiswars)
* Darkest Hour (darkesthour) [[Separate Query Port - Usually port+1](#separate-query-port)]
* Day of Defeat (dod)
* Day of Defeat: Source (dods)
* DayZ (dayz) [[Separate Query Port - Usually 27016-27020](#separate-query-port)]
* Dino D-Day (dinodday)
* Doom 3 (doom3)
* DOTA 2 (dota2)
* Enemy Territory Quake Wars (etqw)
* F.E.A.R. (fear)
* Fortress Forever (fortressforever)
* Frontlines: Fuel of War (ffow)
* Garry's Mod (garrysmod)
* Ghost Recon: Advanced Warfighter (graw)
* Ghost Recon: Advanced Warfighter 2 (graw2)
* Gore (gore)
* Half-Life 1 Deathmatch (hldm)
* Half-Life 2 Deathmatch (hl2dm)
* Halo (halo)
* The Hidden: Source (hidden)
* Just Cause Multiplayer (jcmp)
* Homefront (homefront)
* Insurgency (insurgency)
* Just Cause 2 Multiplayer (jc2mp)
* Killing Floor (killingfloor)
* KzMod (kzmod)
* Left 4 Dead (left4dead)
* Left 4 Dead 2 (left4dead2)
* Mafia 2 Multiplayer (m2mp) [[Separate Query Port - Usually port+1](#separate-query-port)]
* Medal of Honor: Allied Assault (mohaa) [[Separate Query Port - Usually port+97](#separate-query-port)]
* Medal of Honor: Spearhead (mohsh) [[Separate Query Port - Usually port+97](#separate-query-port)]
* Medal of Honor: Breakthrough (mohbt) [[Separate Query Port - Usually port+97](#separate-query-port)]
* Medal of Honor 2010 (moh2010) [[Separate Query Port - Usually 48888](#separate-query-port)]
* Medal of Honor: Warfighter (mohwf)
* Minecraft (minecraft) [[Additional Notes](#minecraft)]
* Monday Night Combat (mnc)
* Multi Theft Auto [[Separate Query Port - Usually port+123](#separate-query-port)]
* Mutant Factions (mutantfactions)
* Natural Selection (ns)
* Natural Selection 2 (ns2) [[Separate Query Port - Usually port+1](#separate-query-port)]
* No More Room in Hell (nmrih)
* Nuclear Dawn (nucleardawn)
* Prey (prey)
* Quake 1 (quake1)
* Quake 2 (quake2)
* Quake 3 (quake3)
* Quake 4 (quake4)
* Red Orchestra: Ostfront 41-45 (redorchestraost) [[Separate Query Port - Usually port+10](#separate-query-port)]
* Red Orchestra 2 (redorchestra2) [[Separate Query Port - Usually 27015](#separate-query-port)]
* Return to Castle Wolfenstein (rtcw)
* Ricochet (ricochet)
* Rust (rust)
* The Ship (ship)
* ShootMania (shootmania) [[Additional Notes](#nadeo-shootmania--trackmania--etc)]
* Soldier of Fortune 2 (sof2)
* Star Wars: Battlefront 2 (swbf2)
* Star Wars: Jedi Knight (swjk)
* Star Wars: Jedi Knight 2 (swjk2)
* Starbound (starbound)
* Suicide Survival (suicidesurvival)
* SWAT 4 (swat4) [[Separate Query Port - Usually port+2](#separate-query-port)]
* Sven Coop (svencoop)
* Synergy (synergy)
* Team Fortress 2 (tf2)
* Team Fortress Classic (tfc)
* Terraria (terraria) [[Additional Notes](#terraria)]
* TrackMania 2 (trackmania2) [[Additional Notes](#nadeo-shootmania--trackmania--etc)]
* TrackMania Forever (trackmaniaforever) [[Additional Notes](#nadeo-shootmania--trackmania--etc)]
* Unreal Tournament (ut) [[Separate Query Port - Usually port+1](#separate-query-port)]
* Unreal Tournament 2004 (ut2004)
* Unreal Tournament 3 (ut3)
* Unreal Tournament 3 (ut3) [[Separate Query Port - Usually 6500](#separate-query-port)]
* Warsow (warsow)
* Wolfenstein 2009 (wolfenstein2009)
* Wolfenstein: Enemy Territory (wolfensteinet)
* Zombie Master (zombiemaster)
* Zombie Panic: Source (zps)
Don't see your game listed here?

View File

@ -2,6 +2,7 @@
ageofchivalry|Age of Chivalry|valve
alienswarm|Alien Swarm|valve
avp2|Aliens vs Predator 2|gamespy1|27888
americasarmy2|America's Army 2|americasarmy
americasarmy3|America's Army 3|valve|27020
americasarmypg|America's Army: Proving Grounds|valve|27020
@ -10,30 +11,80 @@ arma2|ArmA Armed Assault 2|gamespy3|2302
arma3|ArmA Armed Assault 3|gamespy3|2302
bf1942|Battlefield 1942|gamespy1|23000
bf2142|Battlefield 2142|gamespy3|29900
bf3|Battlefield 3|battlefield
bf4|Battlefield 4|battlefield
bfv|Battlefield Vietnam|gamespy2|23000
brink|Brink|valve|27016
cod|Call of Duty|quake3|28960
cod2|Call of Duty 2|quake3|28960
cod4|Call of Duty 4|quake3|28960
codmw3|Call of Duty: Modern Warfare 3|valve|27017
coduo|Call of Duty: United Offensive|quake3|28960
codwaw|Call of Duty: World at War|quake3|28960
csgo|Counter-Strike: Global Offensive|valve
css|Counter-Strike: Source|valve
cs16|Counter-Strike 1.6|valvegold
cs16|Counter-Strike 1.6|valve
cscz|Counter-Strike: Condition Zero|valve
crysis|Crysis|gamespy3|64087
crysis2|Crysis 2|gamespy3|64000
crysiswars|Crysis Wars|gamespy3|64100
darkesthour|Darkest Hour|unreal2|7758
dayz|DayZ|valve|2302
dinodday|Dino D-Day|valve
dod|Day of Defeat|valve
dods|Day of Defeat: Source|valve
doom3|Doom 3|doom3
dota2|DOTA 2|valve
fear|F.E.A.R.|gamespy2|27888
fortressforever|Fortress Forever|valve
garrysmod|Garry's Mod|valve
graw|Ghost Recon: Advanced Warfighter|gamespy2
graw2|Ghost Recon: Advanced Warfighter 2|gamespy2
gore|Gore|gamespy1
hldm|Half-Life 1 Deathmatch|valve
hl2dm|Half-Life 2 Deathmatch|valve
halo|Halo|gamespy2|2302
hidden|The Hidden: Source|valve
homefront|Homefront|valve
insurgency|Insurgency|valve
kzmod|KzMod|valve
left4dead|Left 4 Dead|valve
left4dead2|Left 4 Dead 2|valve
mohaa|Medal of Honor: Allied Assault|gamespy1|12300
mohsh|Medal of Honor: Spearhead|gamespy1|12300
mohbt|Medal of Honor: Breakthrough|gamespy1|12300
moh2010|Medal of Honor 2010|battlefield|48888
mohwf|Medal of Honor: Warfighter|battlefield|25200
mnc|Monday Night Combat|valve
mta|Multi Theft Auto|ase|22126
nmrih|No More Room in Hell|valve
ns|Natural Selection|valvegold
ns|Natural Selection|valve
ns2|Natural Selection 2|valve|27016
nucleardawn|Nuclear Dawn|valve
prey|Prey|doom3|27719
quake2|Quake 2|quake2
quake3|Quake 3|quake3
ricochet|Ricochet|valvegold
redorchestraost|Red Orchestra: Ostfront 41-45|gamespy1|7767
redorchestra2|Red Orchestra 2|valve
rtcw|Return to Castle Wolfenstein|quake3|27960
ricochet|Ricochet|valve
rust|Rust|valve|28016
ship|The Ship|valve
shootmania|Shootmania|nadeo
sof2|Soldier of Fortune 2|quake3|20100
swbf2|Star Wars: Battlefront 2|gamespy2|3658
swjk|Star Wars: Jedi Knight|quake3|29070
swjk2|Star Wars: Jedi Knight 2|quake3|28070
starbound|Starbound|valve|21025
suicidesurvival|Suicide Survival|valve
svencoop|Sven Coop|valvegold
swat4|SWAT 4|gamespy2|10482
svencoop|Sven Coop|valve
synergy|Synergy|valve
tfc|Team Fortress Classic|valve
tf2|Team Fortress 2|valve
trackmania2|Trackmania 2|nadeo
trackmaniaforever|Trackmania Forever|nadeo
ut|Unreal Tournament|gamespy1|7778
wolfensteinet|Wolfenstein: Enemy Territory|quake3|27960
zombiemaster|Zombie Master|valve
zps|Zombie Panic: Source|valve

8
games/bfbc2.js Normal file
View File

@ -0,0 +1,8 @@
module.exports = require('./protocols/battlefield').extend({
init: function() {
this._super();
this.pretty = 'Battlefield: Bad Company 2';
this.options.port = 48888;
this.isBadCompany2 = true;
}
});

11
games/etqw.js Normal file
View File

@ -0,0 +1,11 @@
module.exports = require('./protocols/doom3').extend({
init: function() {
this._super();
this.pretty = 'Enemy Territory Quake Wars';
this.options.port = 27733;
this.isEtqw = true;
this.hasSpaceBeforeClanTag = true;
this.hasClanTag = true;
this.hasTypeFlag = true;
}
});

35
games/ffow.js Normal file
View File

@ -0,0 +1,35 @@
module.exports = require('./protocols/valve').extend({
init: function() {
this._super();
this.pretty = 'Frontlines: Fuel of War';
this.options.port = 5478;
this.byteorder = 'be';
this.legacyChallenge = true;
},
queryInfo: function(state,c) {
var self = this;
self.sendPacket(0x46,false,new Buffer('LSQ'),0x49,function(b) {
var reader = self.reader(b);
state.raw.protocol = reader.uint(1);
state.name = reader.string();
state.map = reader.string();
state.raw.mod = reader.string();
state.raw.gamemode = reader.string();
state.raw.description = reader.string();
state.raw.version = reader.string();
state.raw.port = reader.uint(2);
state.raw.numplayers = reader.uint(1);
state.maxplayers = 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.secure = reader.uint(1);
state.raw.averagefps = reader.uint(1);
state.raw.round = reader.uint(1);
state.raw.maxrounds = reader.uint(1);
state.raw.timeleft = reader.uint(2);
c();
});
}
});

View File

@ -15,7 +15,7 @@ module.exports = require('./protocols/gamespy3').extend({
this._super();
this.options.port = 7777;
this.pretty = 'Just Cause 2 Multiplayer';
this._singlePacketSplits = true;
this.useOnlySingleSplit = true;
},
finalizeState: function(state) {
this._super(state);

39
games/m2mp.js Normal file
View File

@ -0,0 +1,39 @@
module.exports = require('./protocols/core').extend({
init: function() {
this._super();
this.encoding = 'latin1';
this.pretty = 'Mafia 2 Multiplayer';
this.options.port = 27016;
},
run: function(state) {
var self = this;
this.udpSend('M2MP',function(buffer) {
var reader = self.reader(buffer);
var header = reader.string({length:4});
if(header != 'M2MP') return;
state.name = self.readString(reader);
state.raw.numplayers = self.readString(reader);
state.maxplayers = self.readString(reader);
state.raw.gamemode = self.readString(reader);
state.password = !!reader.uint(1);
while(!reader.done()) {
var name = self.readString(reader);
if(!name) break;
state.players.push({
name:name
});
}
self.finish(state);
return true;
});
},
readString: function(reader) {
var length = reader.uint(1);
return reader.string({length:length-1});
},
});

View File

@ -4,6 +4,15 @@ var varint = require('varint'),
function varIntBuffer(num) {
return new Buffer(varint.encode(num));
}
function buildPacket(id,data) {
if(!data) data = new Buffer(0);
var idBuffer = varIntBuffer(id);
return Buffer.concat([
varIntBuffer(data.length+idBuffer.length),
idBuffer,
data
]);
}
module.exports = require('./protocols/core').extend({
init: function() {
@ -32,22 +41,12 @@ module.exports = require('./protocols/core').extend({
portBuf,
varIntBuffer(1)
];
function buildPacket(id,data) {
if(!data) data = new Buffer(0);
var idBuffer = varIntBuffer(id);
return Buffer.concat([
varIntBuffer(data.length+idBuffer.length),
idBuffer,
data
]);
}
var outBuffer = Buffer.concat([
buildPacket(0,Buffer.concat(bufs)),
buildPacket(0)
]);
self.tcpSend(outBuffer, function(data) {
if(data.length < 10) return false;
var expected = varint.decode(data);
@ -55,6 +54,7 @@ module.exports = require('./protocols/core').extend({
if(data.length < expected) return false;
receivedData = data;
c();
return true;
});
},
function(c) {

50
games/protocols/ase.js Normal file
View File

@ -0,0 +1,50 @@
module.exports = require('./core').extend({
init: function() {
this._super();
},
run: function(state) {
var self = this;
self.udpSend('s',function(buffer) {
var reader = self.reader(buffer);
var header = reader.string({length:4});
if(header != 'EYE1') return;
state.raw.gamename = self.readString(reader);
state.raw.port = parseInt(self.readString(reader));
state.name = self.readString(reader);
state.raw.gametype = self.readString(reader);
state.map = self.readString(reader);
state.raw.version = self.readString(reader);
state.password = self.readString(reader) == '1';
state.raw.numplayers = parseInt(self.readString(reader));
state.maxplayers = parseInt(self.readString(reader));
while(!reader.done()) {
var key = self.readString(reader);
if(!key) break;
var value = self.readString(reader);
state.raw[key] = value;
}
console.log(reader.rest());
while(!reader.done()) {
var flags = reader.uint(1);
var player = {};
if(flags & 1) player.name = self.readString(reader);
if(flags & 2) player.team = self.readString(reader);
if(flags & 4) player.skin = self.readString(reader);
if(flags & 8) player.score = parseInt(self.readString(reader));
if(flags & 16) player.ping = parseInt(self.readString(reader));
if(flags & 32) player.time = parseInt(self.readString(reader));
state.players.push(player);
}
self.finish(state);
});
},
readString: function(reader) {
var len = reader.uint(1);
return reader.string({length:len-1});
}
});

View File

@ -0,0 +1,164 @@
var async = require('async');
function buildPacket(params) {
var self = this;
var paramBuffers = [];
params.forEach(function(param) {
paramBuffers.push(new Buffer(param));
});
var totalLength = 12;
paramBuffers.forEach(function(paramBuffer) {
totalLength += paramBuffer.length+1+4;
});
var b = new Buffer(totalLength);
b.writeUInt32LE(0,0);
b.writeUInt32LE(totalLength,4);
b.writeUInt32LE(params.length,8);
var offset = 12;
paramBuffers.forEach(function(paramBuffer) {
b.writeUInt32LE(paramBuffer.length, offset); offset += 4;
paramBuffer.copy(b, offset); offset += paramBuffer.length;
b.writeUInt8(0, offset); offset += 1;
});
return b;
}
module.exports = require('./core').extend({
init: function() {
this._super();
this.encoding = 'latin1';
this.options.port = 25200+22000;
},
run: function(state) {
var self = this;
var decoded;
async.series([
function(c) {
self.query(['serverInfo'], function(data) {
if(self.debug) console.log(data);
if(data.shift() != 'OK') return self.fatal('Missing OK');
state.raw.name = data.shift();
state.raw.numplayers = parseInt(data.shift());
state.maxplayers = parseInt(data.shift());
state.raw.gametype = data.shift();
state.map = data.shift();
state.raw.roundsplayed = parseInt(data.shift());
state.raw.roundstotal = parseInt(data.shift());
var teamCount = data.shift();
state.raw.teams = [];
for(var i = 0; i < teamCount; i++) {
var tickets = parseFloat(data.shift());
state.raw.teams.push({
tickets:tickets
});
}
state.raw.targetscore = parseInt(data.shift());
data.shift();
state.raw.ranked = (data.shift() == 'true');
state.raw.punkbuster = (data.shift() == 'true');
state.password = (data.shift() == 'true');
state.raw.uptime = parseInt(data.shift());
state.raw.roundtime = parseInt(data.shift());
if(self.isBadCompany2) {
data.shift();
data.shift();
}
state.raw.ip = data.shift();
state.raw.punkbusterversion = data.shift();
state.raw.joinqueue = (data.shift() == 'true');
state.raw.region = data.shift();
if(!self.isBadCompany2) {
state.raw.pingsite = data.shift();
state.raw.country = data.shift();
state.raw.quickmatch = (data.shift() == 'true');
}
c();
});
},
function(c) {
self.query(['version'], function(data) {
if(self.debug) console.log(data);
if(data[0] != 'OK') return self.fatal('Missing OK');
state.raw.version = data[2];
c();
});
},
function(c) {
self.query(['listPlayers','all'], function(data) {
if(self.debug) console.log(data);
if(data.shift() != 'OK') return self.fatal('Missing OK');
var fieldCount = parseInt(data.shift());
var fields = [];
for(var i = 0; i < fieldCount; i++) {
fields.push(data.shift());
}
var numplayers = data.shift();
for(var i = 0; i < numplayers; i++) {
var player = {};
fields.forEach(function(key) {
var value = data.shift();
if(key == 'teamId') key = 'team';
else if(key == 'squadId') key = 'squad';
if(
key == 'kills'
|| key == 'deaths'
|| key == 'score'
|| key == 'rank'
|| key == 'team'
|| key == 'squad'
|| key == 'ping'
|| key == 'type'
) {
value = parseInt(value);
}
player[key] = value;
});
state.players.push(player);
}
self.finish(state);
});
}
]);
},
query: function(params,c) {
var self = this;
this.tcpSend(buildPacket(params), function(data) {
var decoded = self.decodePacket(data);
if(!decoded) return false;
c(decoded);
return true;
});
},
decodePacket: function(buffer) {
if(buffer.length < 8) return false;
var reader = this.reader(buffer);
var header = reader.uint(4);
var totalLength = reader.uint(4);
if(buffer.length < totalLength) return false;
var paramCount = reader.uint(4);
var params = [];
for(var i = 0; i < paramCount; i++) {
var len = reader.uint(4);
params.push(reader.string({length:len}));
var strNull = reader.uint(1);
}
return params;
}
});

View File

@ -165,6 +165,18 @@ module.exports = Class.extend(EventEmitter,{
return id;
},
trueTest: function(str) {
if(typeof str == 'boolean') return str;
if(typeof str == 'number') return str != 0;
if(typeof str == 'string') {
if(str.toLowerCase() == 'true') return true;
if(str == 'yes') return true;
if(str == '1') return true;
}
return false;
},
@ -194,10 +206,13 @@ module.exports = Class.extend(EventEmitter,{
});
},
tcpSend: function(buffer,ondata) {
if(this.tcpCallback) return this.fatal('Attempted to send TCP packet while still waiting on a managed response');
this.tcpCallback = ondata;
this._tcpConnect(function(socket) {
socket.write(buffer);
var self = this;
process.nextTick(function() {
if(self.tcpCallback) return self.fatal('Attempted to send TCP packet while still waiting on a managed response');
self.tcpCallback = ondata;
self._tcpConnect(function(socket) {
socket.write(buffer);
});
});
},
@ -205,18 +220,19 @@ module.exports = Class.extend(EventEmitter,{
udpSend: function(buffer,onpacket,ontimeout) {
var self = this;
process.nextTick(function() {
if(self.udpCallback) return self.fatal('Attempted to send UDP packet while still waiting on a managed response');
self._udpSendNow(buffer);
if(!onpacket) return;
if(self.udpCallback) return self.fatal('Attempted to send UDP packet while still waiting on a managed response');
self._udpSendNow(buffer);
if(!onpacket) return;
self.udpTimeoutTimer = self.setTimeout(function() {
self.udpCallback = false;
var timeout = false;
if(!ontimeout || ontimeout() !== true) timeout = true;
if(timeout) self.error('timeout');
},1000);
self.udpCallback = onpacket;
self.udpTimeoutTimer = self.setTimeout(function() {
self.udpCallback = false;
var timeout = false;
if(!ontimeout || ontimeout() !== true) timeout = true;
if(timeout) self.error('timeout');
},1000);
self.udpCallback = onpacket;
});
},
_udpSendNow: function(buffer) {
if(!('port' in this.options)) return this.fatal('Attempted to send without setting a port');

96
games/protocols/doom3.js Normal file
View File

@ -0,0 +1,96 @@
module.exports = require('./core').extend({
init: function() {
this._super();
this.pretty = 'Doom 3';
this.options.port = 27666;
this.encoding = 'latin1';
this.isEtqw = false;
this.hasSpaceBeforeClanTag = false;
this.hasClanTag = false;
this.hasTypeFlag = false;
},
run: function(state) {
var self = this;
this.udpSend('\xff\xffgetInfo\x00PiNGPoNG\x00',function(buffer) {
var reader = self.reader(buffer);
var header = reader.uint(2);
if(header != 0xffff) return;
var header2 = reader.string();
if(header2 != 'infoResponse') return;
var tailSize = 5;
if(self.isEtqw) {
var taskId = reader.uint(4);
}
var challenge = reader.uint(4);
var protoVersion = reader.uint(4);
state.raw.protocolVersion = (protoVersion>>16)+'.'+(protoVersion&0xffff);
if(self.isEtqw) {
var size = reader.uint(4);
}
while(!reader.done()) {
var key = reader.string();
var value = self.stripColors(reader.string());
if(key == 'si_map') {
value = value.replace('maps/','');
value = value.replace('.entities','');
}
if(!key) break;
state.raw[key] = value;
}
var i = 0;
while(!reader.done()) {
i++;
var player = {};
player.id = reader.uint(1);
if(player.id == 32) break;
player.ping = reader.uint(2);
if(!self.isEtqw) player.rate = reader.uint(4);
player.name = self.stripColors(reader.string());
if(self.hasClanTag) {
if(self.hasSpaceBeforeClanTag) reader.uint(1);
player.clantag = self.stripColors(reader.string());
}
if(self.hasTypeFlag) player.typeflag = reader.uint(1);
if(!player.ping || player.typeflag)
state.bots.push(player);
else
state.players.push(player);
}
state.raw.osmask = reader.uint(4);
if(self.isEtqw) {
state.raw.ranked = reader.uint(1);
state.raw.timeleft = reader.uint(4);
state.raw.gamestate = reader.uint(1);
state.raw.servertype = reader.uint(1);
// 0 = regular, 1 = tv
if(state.raw.servertype == 0) {
state.raw.interestedClients = reader.uint(1);
} else if(state.raw.servertype == 1) {
state.raw.connectedClients = reader.uint(4);
state.raw.maxClients = reader.uint(4);
}
}
if(state.raw.si_name) state.name = state.raw.si_name;
if(state.raw.si_map) state.map = state.raw.si_map;
if(state.raw.si_maxplayers) state.maxplayers = parseInt(state.raw.si_maxplayers);
if(state.raw.si_usepass == '1') state.password = true;
self.finish(state);
return true;
});
},
stripColors: function(str) {
// uses quake 3 color codes
return str.replace(/\^(X.{6}|.)/g,'');
}
});

View File

@ -16,7 +16,7 @@ module.exports = require('./core').extend({
state.raw = data;
if('hostname' in state.raw) state.name = state.raw.hostname;
if('mapname' in state.raw) state.map = state.raw.mapname;
if(state.raw.password == '1') state.password = true;
if(self.trueTest(state.raw.password)) state.password = true;
if('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers);
c();
});
@ -42,7 +42,7 @@ module.exports = require('./core').extend({
} else {
if(!(id in players)) players[id] = {};
if(key == 'playername') key = 'name';
else if(key == 'team') value = parseInt(value)-1;
else if(key == 'team') value = parseInt(value);
else if(key == 'score' || key == 'ping' || key == 'deaths') value = parseInt(value);
players[id][key] = value;
}

View File

@ -9,31 +9,41 @@ module.exports = require('./core').extend({
var self = this;
var request = new Buffer([0xfe,0xfd,0x00,0x00,0x00,0x00,0x01,0xff,0xff,0xff]);
this.udpSend(request,function(buffer) {
var reader = self.reader(buffer);
var header = reader.uint(1);
if(header != 0) return;
var pingId = reader.uint(4);
if(pingId != 1) return;
while(!reader.done()) {
var key = reader.string();
var value = reader.string();
if(!key) break;
state.raw[key] = value;
var packets = [];
this.udpSend(request,
function(buffer) {
if(packets.length && buffer.readUInt8(0) == 0)
buffer = buffer.slice(1);
packets.push(buffer);
},
function() {
var buffer = Buffer.concat(packets);
console.log(buffer.toString());
var reader = self.reader(buffer);
var header = reader.uint(1);
if(header != 0) return;
var pingId = reader.uint(4);
if(pingId != 1) return;
while(!reader.done()) {
var key = reader.string();
var value = reader.string();
if(!key) break;
state.raw[key] = value;
}
if('hostname' in state.raw) state.name = state.raw.hostname;
if('mapname' in state.raw) state.map = state.raw.mapname;
if(self.trueTest(state.raw.password)) state.password = true;
if('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers);
state.players = self.readFieldData(reader);
state.raw.teams = self.readFieldData(reader);
self.finish(state);
return true;
}
if('hostname' in state.raw) state.name = state.raw.hostname;
if('mapname' in state.raw) state.map = state.raw.mapname;
if(state.raw.password == '1') state.password = true;
if('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers);
state.players = self.readFieldData(reader);
state.raw.teams = self.readFieldData(reader);
self.finish(state);
return true;
});
);
},
readFieldData: function(reader) {
var count = reader.uint(1);
@ -68,6 +78,10 @@ module.exports = require('./core').extend({
else if(key == 'kills_') key = 'kills';
else if(key == 'team_t') key = 'name';
else if(key == 'tickets_t') key = 'tickets';
if(key == 'score' || key == 'deaths' || key == 'ping' || key == 'team'
|| key == 'kills' || key == 'tickets') value = parseInt(value);
unit[key] = value;
}
units.push(unit);

View File

@ -7,6 +7,7 @@ module.exports = require('./core').extend({
this.encoding = 'latin1';
this.byteorder = 'be';
this.noChallenge = false;
this.useOnlySingleSplit = false;
},
run: function(state) {
var self = this;
@ -17,7 +18,6 @@ module.exports = require('./core').extend({
if(self.noChallenge) return c();
self.sendPacket(9,false,false,false,function(buffer) {
var reader = self.reader(buffer);
reader.skip(5);
challenge = parseInt(reader.string());
c();
});
@ -125,16 +125,25 @@ module.exports = require('./core').extend({
if(iSessionId != self.sessionId) return;
if(!assemble) {
c(buffer);
c(buffer.slice(5));
return true;
}
if(self.useOnlySingleSplit) {
// has split headers, but they are worthless and only one packet is used
c([buffer.slice(16)]);
return true;
}
var id = buffer.readUInt16LE(14);
var last = (id & 0x80);
id = id & 0x7f;
if(last || self._singlePacketSplits) numPackets = id+1;
if(last) numPackets = id+1;
packets[id] = buffer.slice(16);
if(self.debug) {
console.log("Received packet #"+id);
if(last) console.log("(last)");
}
if(!numPackets || Object.keys(packets).length != numPackets) return;

View File

@ -7,17 +7,28 @@ module.exports = require('./core').extend({
this.delimiter = '\n';
this.sendHeader = 'status';
this.responseHeader = 'print';
this.isQuake1 = false;
},
run: function(state) {
var self = this;
this.udpSend('\xff\xff\xff\xff'+this.sendHeader+'\x00',function(buffer) {
var reader = self.reader(buffer);
var header = reader.string();
if(header != '\xff\xff\xff\xff'+this.responseHeader) return;
var header = reader.string({length:4});
if(header != '\xff\xff\xff\xff') return;
var response;
if(this.isQuake1) {
response = reader.string({length:1});
} else {
response = reader.string();
}
if(response != this.responseHeader) return;
var info = reader.string().split('\\');
if(info[0] == '') info.shift();
while(true) {
var key = info.shift();
var value = info.shift();
@ -26,10 +37,11 @@ module.exports = require('./core').extend({
}
while(!reader.done()) {
var player = reader.string();
var line = reader.string();
if(!line || line.charAt(0) == '\0') break;
var args = [];
var split = player.split('"');
var split = line.split('"');
var inQuote = false;
split.forEach(function(part,i) {
var inQuote = (i%2 == 1);
@ -43,14 +55,24 @@ module.exports = require('./core').extend({
}
});
var frags = parseInt(args[0]);
var ping = parseInt(args[1]);
var name = args[2] || '';
var address = args[3] || '';
var player = {};
if(self.isQuake1) {
player.id = parseInt(args.shift());
player.score = parseInt(args.shift());
player.time = parseInt(args.shift());
player.ping = parseInt(args.shift());
player.name = args.shift();
player.skin = args.shift();
player.color1 = parseInt(args.shift());
player.color2 = parseInt(args.shift());
} else {
player.frags = parseInt(args.shift());
player.ping = parseInt(args.shift());
player.name = args.shift() || '';
player.address = args.shift() || '';
}
(ping == 0 ? state.bots : state.players).push({
frags:frags, ping:ping, name:name, address:address
});
(player.ping ? state.players : state.bots).push(player);
}
if('g_needpass' in state.raw) state.password = state.raw.g_needpass;

View File

@ -5,168 +5,189 @@ module.exports = require('./core').extend({
init: function() {
this._super();
this.goldsrc = false;
this.legacyChallenge = false;
this.options.port = 27015;
// 2006 engines don't pass packet switching size in split packet header
// while all others do
this._skipSizeInSplitHeader = false;
this._challenge = '';
},
run: function(state) {
var self = this;
var challenge;
async.series([
function(c) {
self.sendPacket(
0x54,false,new Buffer('Source Engine Query\0'),
self.goldsrc ? 0x6D : 0x49,
function(b) {
var reader = self.reader(b);
if(self.goldsrc) state.raw.address = reader.string();
else state.raw.protocol = reader.uint(1);
state.name = reader.string();
state.map = reader.string();
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.raw.protocol = reader.uint(1);
else state.raw.numbots = 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) {
state.raw.modlink = reader.string();
state.raw.moddownload = reader.string();
reader.skip(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.raw.secure = reader.uint(1);
if(self.goldsrc) {
state.raw.numbots = reader.uint(1);
} else {
if(state.raw.folder == 'ship') {
state.raw.shipmode = reader.uint(1);
state.raw.shipwitnesses = reader.uint(1);
state.raw.shipduration = reader.uint(1);
}
state.raw.version = reader.string();
var extraFlag = reader.uint(1);
if(extraFlag & 0x80) state.raw.port = reader.uint(2);
if(extraFlag & 0x10) state.raw.steamid = reader.uint(8);
if(extraFlag & 0x40) {
state.raw.sourcetvport = reader.uint(2);
state.raw.sourcetvname = reader.string();
}
if(extraFlag & 0x20) state.raw.tags = reader.string();
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(self.goldsrc?0x56:0x55,0xffffffff,false,0x41,function(b) {
var reader = self.reader(b);
challenge = reader.uint(4);
c();
});
},
function(c) {
self.sendPacket(0x55,challenge,false,0x44,function(b) {
var reader = self.reader(b);
var num = reader.uint(1);
for(var i = 0; i < num; i++) {
reader.skip(1);
var name = reader.string();
var score = reader.int(4);
var time = reader.float();
// connecting players don't could as players.
if(!name) continue;
(time == -1 ? state.bots : state.players).push({
name:name, score:score, time:time
});
}
// if we didn't find the bots, iterate
// through and guess which ones they are
if(!state.bots.length) {
var maxTime = 0;
state.players.forEach(function(player) {
maxTime = Math.max(player.time,maxTime);
});
for(var i = 0; i < state.players.length; i++) {
var player = state.players[i];
if(state.bots.length >= state.raw.numbots) continue;
if(player.time != maxTime) continue;
state.bots.push(player);
state.players.splice(i, 1);
i--;
}
}
c();
});
},
function(c) {
self.sendPacket(0x56,challenge,false,0x45,function(b) {
var reader = self.reader(b);
var num = reader.uint(2);
state.raw.rules = [];
for(var i = 0; i < num; i++) {
var key = reader.string();
var value = reader.string();
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) {
self.finish(state);
}
function(c) { self.queryInfo(state,c); },
function(c) { self.queryChallenge(state,c); },
function(c) { self.queryPlayers(state,c); },
function(c) { self.queryRules(state,c); },
function(c) { self.finish(state); }
]);
},
sendPacket: function(type,challenge,payload,expect,callback,ontimeout) {
queryInfo: function(state,c) {
var self = this;
self.sendPacket(
0x54,false,'Source Engine Query\0',
self.goldsrc ? 0x6D : 0x49,
function(b) {
var reader = self.reader(b);
if(self.goldsrc) state.raw.address = reader.string();
else state.raw.protocol = reader.uint(1);
state.name = reader.string();
state.map = reader.string();
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.raw.protocol = reader.uint(1);
else state.raw.numbots = 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) {
state.raw.modlink = reader.string();
state.raw.moddownload = reader.string();
reader.skip(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.raw.secure = reader.uint(1);
if(self.goldsrc) {
state.raw.numbots = reader.uint(1);
} else {
if(state.raw.folder == 'ship') {
state.raw.shipmode = reader.uint(1);
state.raw.shipwitnesses = reader.uint(1);
state.raw.shipduration = reader.uint(1);
}
state.raw.version = reader.string();
var extraFlag = reader.uint(1);
if(extraFlag & 0x80) state.raw.port = reader.uint(2);
if(extraFlag & 0x10) state.raw.steamid = reader.uint(8);
if(extraFlag & 0x40) {
state.raw.sourcetvport = reader.uint(2);
state.raw.sourcetvname = reader.string();
}
if(extraFlag & 0x20) state.raw.tags = reader.string();
if(extraFlag & 0x01) state.raw.gameid = reader.uint(8);
}
if(state.raw.protocol == 7 && state.raw.steamappid == 215) {
self._skipSizeInSplitHeader = true;
}
c();
}
);
},
queryChallenge: function(state,c) {
var self = this;
if(this.legacyChallenge) {
self.sendPacket(0x57,false,false,0x41,function(b) {
var reader = self.reader(b);
self._challenge = reader.uint(4);
c();
});
} else {
self.sendPacket(self.goldsrc?0x56:0x55,0xffffffff,false,0x41,function(b) {
var reader = self.reader(b);
self._challenge = reader.uint(4);
c();
});
}
},
queryPlayers: function(state,c) {
var self = this;
self.sendPacket(0x55,true,false,0x44,function(b) {
var reader = self.reader(b);
var num = reader.uint(1);
for(var i = 0; i < num; i++) {
reader.skip(1);
var name = reader.string();
var score = reader.int(4);
var time = reader.float();
// connecting players don't could as players.
if(!name) continue;
(time == -1 ? state.bots : state.players).push({
name:name, score:score, time:time
});
}
// if we didn't find the bots, iterate
// through and guess which ones they are
if(!state.bots.length && state.raw.numbots) {
var maxTime = 0;
state.players.forEach(function(player) {
maxTime = Math.max(player.time,maxTime);
});
for(var i = 0; i < state.players.length; i++) {
var player = state.players[i];
if(state.bots.length >= state.raw.numbots) continue;
if(player.time != maxTime) continue;
state.bots.push(player);
state.players.splice(i, 1);
i--;
}
}
c();
});
},
queryRules: function(state,c) {
var self = this;
self.sendPacket(0x56,true,false,0x45,function(b) {
var reader = self.reader(b);
var num = reader.uint(2);
state.raw.rules = {};
for(var i = 0; i < num; i++) {
var key = reader.string();
var value = reader.string();
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;
});
},
sendPacket: function(type,sendChallenge,payload,expect,callback,ontimeout) {
var self = this;
var challengeLength = challenge === false ? 0 : 4;
if(typeof payload == 'string') payload = new Buffer(payload);
var challengeLength = sendChallenge !== false ? 4 : 0;
var payloadLength = payload ? payload.length : 0;
var b = new Buffer(5 + challengeLength + payloadLength);
b.writeInt32LE(-1, 0);
b.writeUInt8(type, 4);
if(challengeLength) b.writeUInt32LE(challenge, 5);
if(sendChallenge !== false) {
var challenge = this._challenge;
if(typeof sendChallenge == 'number') challenge = sendChallenge;
if(self.byteorder == 'le') b.writeUInt32LE(challenge, 5);
else b.writeUInt32BE(challenge, 5);
}
if(payloadLength) payload.copy(b, 5+challengeLength);
function received(payload) {
@ -210,6 +231,7 @@ module.exports = require('./core').extend({
if(self.debug) {
console.log("Received partial packet id: "+id);
console.log("Expecting "+numPackets+" packets, have "+Object.keys(packets).length);
console.log("Bzip? "+bzip);
}
if(!numPackets || Object.keys(packets).length != numPackets) return;

9
games/quake1.js Normal file
View File

@ -0,0 +1,9 @@
module.exports = require('./protocols/quake2').extend({
init: function() {
this._super();
this.pretty = 'Quake 1';
this.options.port = 27500;
this.responseHeader = 'n';
this.isQuake1 = true;
}
});

8
games/quake4.js Normal file
View File

@ -0,0 +1,8 @@
module.exports = require('./protocols/doom3').extend({
init: function() {
this._super();
this.pretty = 'Quake 4';
this.hasClanTag = true;
this.options.port = 28004;
}
});

12
games/wolfenstein2009.js Normal file
View File

@ -0,0 +1,12 @@
// this was assembled from old docs and not tested
// hopefully it still works
module.exports = require('./protocols/doom3').extend({
init: function() {
this._super();
this.pretty = 'Wolfenstein 2009';
this.hasSpaceBeforeClanTag = true;
this.hasClanTag = true;
this.hasTypeFlag = true;
}
});

View File

@ -42,14 +42,19 @@ Reader.prototype = {
var delim = options.delimiter || this.query.delimiter;
if(typeof delim == 'string') delim = delim.charCodeAt(0);
while(true) {
if(end >= this.buffer.length) return '';
if(end >= this.buffer.length) {
end = this.buffer.length;
break;
}
if(this.buffer.readUInt8(end) == delim) break;
end++;
}
this.i = end+1;
} else {
end = start+options.length;
if(end > this.buffer.length) return '';
if(end >= this.buffer.length) {
end = this.buffer.length;
}
this.i = end;
}
@ -64,7 +69,7 @@ Reader.prototype = {
},
int: function(bytes) {
var r = 0;
if(this.i+bytes <= this.buffer.length) {
if(this.remaining() >= bytes) {
if(this.query.byteorder == 'be') {
if(bytes == 1) r = this.buffer.readInt8(this.i);
else if(bytes == 2) r = this.buffer.readInt16BE(this.i);
@ -80,7 +85,7 @@ Reader.prototype = {
},
uint: function(bytes) {
var r = 0;
if(this.i+bytes <= this.buffer.length) {
if(this.remaining() >= bytes) {
if(this.query.byteorder == 'be') {
if(bytes == 1) r = this.buffer.readUInt8(this.i);
else if(bytes == 2) r = this.buffer.readUInt16BE(this.i);
@ -98,13 +103,16 @@ Reader.prototype = {
},
float: function() {
var r = 0;
if(this.i+4 <= this.buffer.length) {
if(this.remaining() >= 4) {
if(this.query.byteorder == 'be') r = this.buffer.readFloatBE(this.i);
else r = this.buffer.readFloatLE(this.i);
}
this.i += 4;
return r;
},
remaining: function() {
return this.buffer.length-this.i;
},
rest: function() {
return this.buffer.slice(this.i);
},