From e23aa6cf9cf169dec8bd142a33f100c6f43ecc8d Mon Sep 17 00:00:00 2001 From: Michael Morrison Date: Sat, 1 Feb 2014 17:46:10 -0600 Subject: [PATCH] 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 --- README.md | 62 ++++++- games/aliases.txt | 59 ++++++- games/bfbc2.js | 8 + games/etqw.js | 11 ++ games/ffow.js | 35 ++++ games/{jcmp.js => jc2mp.js} | 2 +- games/m2mp.js | 39 +++++ games/minecraftping.js | 22 +-- games/protocols/ase.js | 50 ++++++ games/protocols/battlefield.js | 164 +++++++++++++++++ games/protocols/core.js | 46 +++-- games/protocols/doom3.js | 96 ++++++++++ games/protocols/gamespy1.js | 4 +- games/protocols/gamespy2.js | 62 ++++--- games/protocols/gamespy3.js | 15 +- games/protocols/quake2.js | 44 +++-- games/protocols/valve.js | 310 ++++++++++++++++++--------------- games/quake1.js | 9 + games/quake4.js | 8 + games/wolfenstein2009.js | 12 ++ lib/reader.js | 18 +- 21 files changed, 854 insertions(+), 222 deletions(-) create mode 100644 games/bfbc2.js create mode 100644 games/etqw.js create mode 100644 games/ffow.js rename games/{jcmp.js => jc2mp.js} (91%) create mode 100644 games/m2mp.js create mode 100644 games/protocols/ase.js create mode 100644 games/protocols/battlefield.js create mode 100644 games/protocols/doom3.js create mode 100644 games/quake1.js create mode 100644 games/quake4.js create mode 100644 games/wolfenstein2009.js diff --git a/README.md b/README.md index 70a8680..767dcd9 100644 --- a/README.md +++ b/README.md @@ -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? diff --git a/games/aliases.txt b/games/aliases.txt index a96fff3..6bdc3e7 100644 --- a/games/aliases.txt +++ b/games/aliases.txt @@ -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 diff --git a/games/bfbc2.js b/games/bfbc2.js new file mode 100644 index 0000000..e4939cd --- /dev/null +++ b/games/bfbc2.js @@ -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; + } +}); diff --git a/games/etqw.js b/games/etqw.js new file mode 100644 index 0000000..eba71ab --- /dev/null +++ b/games/etqw.js @@ -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; + } +}); diff --git a/games/ffow.js b/games/ffow.js new file mode 100644 index 0000000..eced02c --- /dev/null +++ b/games/ffow.js @@ -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(); + }); + } +}); diff --git a/games/jcmp.js b/games/jc2mp.js similarity index 91% rename from games/jcmp.js rename to games/jc2mp.js index ea670d6..41fb5a3 100644 --- a/games/jcmp.js +++ b/games/jc2mp.js @@ -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); diff --git a/games/m2mp.js b/games/m2mp.js new file mode 100644 index 0000000..52a75de --- /dev/null +++ b/games/m2mp.js @@ -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}); + }, +}); diff --git a/games/minecraftping.js b/games/minecraftping.js index 349146d..2722b90 100644 --- a/games/minecraftping.js +++ b/games/minecraftping.js @@ -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) { diff --git a/games/protocols/ase.js b/games/protocols/ase.js new file mode 100644 index 0000000..d7d72ed --- /dev/null +++ b/games/protocols/ase.js @@ -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}); + } +}); diff --git a/games/protocols/battlefield.js b/games/protocols/battlefield.js new file mode 100644 index 0000000..bf4dfc4 --- /dev/null +++ b/games/protocols/battlefield.js @@ -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; + } +}); diff --git a/games/protocols/core.js b/games/protocols/core.js index a722068..ea05f58 100644 --- a/games/protocols/core.js +++ b/games/protocols/core.js @@ -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'); diff --git a/games/protocols/doom3.js b/games/protocols/doom3.js new file mode 100644 index 0000000..539e2fa --- /dev/null +++ b/games/protocols/doom3.js @@ -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,''); + } +}); diff --git a/games/protocols/gamespy1.js b/games/protocols/gamespy1.js index bd4fd65..f2379c4 100644 --- a/games/protocols/gamespy1.js +++ b/games/protocols/gamespy1.js @@ -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; } diff --git a/games/protocols/gamespy2.js b/games/protocols/gamespy2.js index c85724b..e4e6db3 100644 --- a/games/protocols/gamespy2.js +++ b/games/protocols/gamespy2.js @@ -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); diff --git a/games/protocols/gamespy3.js b/games/protocols/gamespy3.js index ce74b5f..6d57775 100644 --- a/games/protocols/gamespy3.js +++ b/games/protocols/gamespy3.js @@ -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; diff --git a/games/protocols/quake2.js b/games/protocols/quake2.js index 9625243..9d9e21a 100644 --- a/games/protocols/quake2.js +++ b/games/protocols/quake2.js @@ -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; diff --git a/games/protocols/valve.js b/games/protocols/valve.js index 42787ed..427f6f1 100644 --- a/games/protocols/valve.js +++ b/games/protocols/valve.js @@ -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; diff --git a/games/quake1.js b/games/quake1.js new file mode 100644 index 0000000..c53847a --- /dev/null +++ b/games/quake1.js @@ -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; + } +}); diff --git a/games/quake4.js b/games/quake4.js new file mode 100644 index 0000000..4727e6c --- /dev/null +++ b/games/quake4.js @@ -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; + } +}); diff --git a/games/wolfenstein2009.js b/games/wolfenstein2009.js new file mode 100644 index 0000000..669d3a0 --- /dev/null +++ b/games/wolfenstein2009.js @@ -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; + } +}); diff --git a/lib/reader.js b/lib/reader.js index c5404e9..4046c57 100644 --- a/lib/reader.js +++ b/lib/reader.js @@ -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); },