diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f729351..f0360d1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,10 @@ The Specialists, Vampire Slayer, Warfork (2018), Wurm Unlimited (2015). * Also added support: The Forest (2014), Operation: Harsh Doorstop (2023), Insurgency: Modern Infantry Combat (2007), Counter-Strike 2 (2023). * Capitalized 'Unturned' in game.txt +* Removed the players::setNum method, the library will no longer add empty players as +a placeholder in the `players` field. +* Fixed wrong field being parsed for `maxplayers` on Doom3. +* Stabilized field `numplayers`. ### 4.1.0 * Replace `compressjs` dependency by `seek-bzip` to solve some possible import issues. diff --git a/README.md b/README.md index 542f88e4..6a608863 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ The returned state object will contain the following keys: * **name**: string - Server name * **map**: string - Current server game map * **password**: boolean - If a password is required +* **numplayers**: number * **maxplayers**: number * **players**: array of objects * **name**: string - If the player's name is unknown, the string will be empty. diff --git a/lib/Results.js b/lib/Results.js index fd76ebd9..7d8abf90 100644 --- a/lib/Results.js +++ b/lib/Results.js @@ -14,16 +14,6 @@ export class Player { } export class Players extends Array { - setNum (num) { - // If the server specified some ridiculous number of players (billions), we don't want to - // run out of ram allocating these objects. - num = Math.min(num, 10000) - - while (this.length < num) { - this.push({}) - } - } - push (data) { super.push(new Player(data)) } diff --git a/protocols/armagetron.js b/protocols/armagetron.js index 52ff3797..11c065fd 100644 --- a/protocols/armagetron.js +++ b/protocols/armagetron.js @@ -18,7 +18,7 @@ export default class armagetron extends Core { state.gamePort = this.readUInt(reader) state.raw.hostname = this.readString(reader) state.name = this.stripColorCodes(this.readString(reader)) - state.raw.numplayers = this.readUInt(reader) + state.numplayers = this.readUInt(reader) state.raw.versionmin = this.readUInt(reader) state.raw.versionmax = this.readUInt(reader) state.raw.version = this.readString(reader) @@ -42,7 +42,7 @@ export default class armagetron extends Core { const a = reader.uint(2) const b = reader.uint(2) return (b << 16) + a - } + } readString (reader) { const len = reader.uint(2) @@ -57,7 +57,7 @@ export default class armagetron extends Core { } return out - } + } stripColorCodes (str) { return str.replace(/0x[0-9a-f]{6}/g, '') diff --git a/protocols/ase.js b/protocols/ase.js index dedec85b..b96bfdfe 100644 --- a/protocols/ase.js +++ b/protocols/ase.js @@ -16,7 +16,7 @@ export default class ase extends Core { state.map = this.readString(reader) state.raw.version = this.readString(reader) state.password = this.readString(reader) === '1' - state.raw.numplayers = parseInt(this.readString(reader)) + state.numplayers = parseInt(this.readString(reader)) state.maxplayers = parseInt(this.readString(reader)) while (!reader.done()) { diff --git a/protocols/assettocorsa.js b/protocols/assettocorsa.js index a036dda8..ad14a6b8 100644 --- a/protocols/assettocorsa.js +++ b/protocols/assettocorsa.js @@ -34,5 +34,7 @@ export default class assettocorsa extends Core { }) } } + + state.numplayers = carInfo.Cars.length } } diff --git a/protocols/battlefield.js b/protocols/battlefield.js index 65c336e4..e4c08b52 100644 --- a/protocols/battlefield.js +++ b/protocols/battlefield.js @@ -11,7 +11,7 @@ export default class battlefield extends Core { { const data = await this.query(socket, ['serverInfo']) state.name = data.shift() - state.raw.numplayers = parseInt(data.shift()) + state.numplayers = parseInt(data.shift()) state.maxplayers = parseInt(data.shift()) state.raw.gametype = data.shift() state.map = data.shift() diff --git a/protocols/buildandshoot.js b/protocols/buildandshoot.js index 7b8b64cc..e1bc1644 100644 --- a/protocols/buildandshoot.js +++ b/protocols/buildandshoot.js @@ -20,7 +20,7 @@ export default class buildandshoot extends Core { m = body.match(/Current players: (\d+)\/(\d+)/) if (m) { - state.raw.numplayers = m[1] + state.numplayers = parseInt(m[1]) state.maxplayers = m[2] } diff --git a/protocols/cs2d.js b/protocols/cs2d.js index 5343cd0c..ca9a5d18 100644 --- a/protocols/cs2d.js +++ b/protocols/cs2d.js @@ -17,7 +17,7 @@ export default class cs2d extends Core { state.raw.forceLight = this.readFlag(flags, 7) state.name = this.readString(reader) state.map = this.readString(reader) - state.raw.numplayers = reader.uint(1) + state.numplayers = reader.uint(1) state.maxplayers = reader.uint(1) if (flags & 32) { state.raw.gamemode = reader.uint(1) diff --git a/protocols/doom3.js b/protocols/doom3.js index 787c71d0..2344e75c 100644 --- a/protocols/doom3.js +++ b/protocols/doom3.js @@ -4,7 +4,7 @@ export default class doom3 extends Core { constructor () { super() this.encoding = 'latin1' - } + } async run (state) { const body = await this.udpSend('\xff\xffgetInfo\x00PiNGPoNg\x00', packet => { @@ -60,6 +60,7 @@ export default class doom3 extends Core { let players; [players, reader] = playerResult + state.numplayers = players.length for (const player of players) { if (!player.ping || player.typeflag) { state.bots.push(player) } else { state.players.push(player) } } @@ -82,7 +83,7 @@ export default class doom3 extends Core { 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_maxPlayers) state.maxplayers = parseInt(state.raw.si_maxplayers) + if (state.raw.si_maxPlayers) state.maxplayers = parseInt(state.raw.si_maxPlayers) if (state.raw.si_usepass === '1') state.password = true if (state.raw.si_needPass === '1') state.password = true if (this.options.port === 27733) state.gamePort = 3074 // etqw has a different query and game port diff --git a/protocols/eco.js b/protocols/eco.js index 6bae1914..ed3a2627 100644 --- a/protocols/eco.js +++ b/protocols/eco.js @@ -11,6 +11,7 @@ export default class eco extends Core { const serverInfo = request.Info state.name = serverInfo.Description + state.numplayers = serverInfo.OnlinePlayers; state.maxplayers = serverInfo.TotalPlayers state.password = serverInfo.HasPassword state.gamePort = serverInfo.GamePort diff --git a/protocols/ffow.js b/protocols/ffow.js index bfcc64fb..3fbe9c7a 100644 --- a/protocols/ffow.js +++ b/protocols/ffow.js @@ -5,7 +5,7 @@ export default class ffow extends valve { super() this.byteorder = 'be' this.legacyChallenge = true - } + } async queryInfo (state) { this.logger.debug('Requesting ffow info ...') @@ -24,7 +24,7 @@ export default class ffow extends valve { state.raw.description = reader.string() state.raw.version = reader.string() state.gamePort = reader.uint(2) - state.raw.numplayers = reader.uint(1) + state.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)) diff --git a/protocols/gamespy1.js b/protocols/gamespy1.js index c80b1f74..1723d1f8 100644 --- a/protocols/gamespy1.js +++ b/protocols/gamespy1.js @@ -111,6 +111,8 @@ export default class gamespy1 extends Core { state.players.push(player) } + + state.numplayers = state.players.length } async sendPacket (type) { diff --git a/protocols/gamespy2.js b/protocols/gamespy2.js index 1ad8f303..7afad957 100644 --- a/protocols/gamespy2.js +++ b/protocols/gamespy2.js @@ -32,6 +32,9 @@ export default class gamespy2 extends Core { for (const rawPlayer of this.readFieldData(reader)) { state.players.push(rawPlayer) } + + if ('numplayers' in state.raw) state.numplayers = parseInt(state.raw.numplayers) + else state.numplayers = state.players.length } // Parse teams diff --git a/protocols/gamespy3.js b/protocols/gamespy3.js index 3825b35e..9187b71d 100644 --- a/protocols/gamespy3.js +++ b/protocols/gamespy3.js @@ -127,6 +127,9 @@ export default class gamespy3 extends Core { state.players.push(player) } } + + if ('numplayers' in state.raw) state.numplayers = parseInt(state.raw.numplayers) + else state.numplayers = state.players.length } async sendPacket (type, challenge, payload, assemble) { diff --git a/protocols/geneshift.js b/protocols/geneshift.js index 12f709b3..4d7608c5 100644 --- a/protocols/geneshift.js +++ b/protocols/geneshift.js @@ -28,7 +28,7 @@ export default class geneshift extends Core { state.raw.country = found[1] state.name = found[4] state.map = found[5] - state.players.setNum(parseInt(found[6])) + state.numplayers = parseInt(found[6]) state.maxplayers = parseInt(found[7]) // fields[8] is unknown? state.raw.rules = found[9] diff --git a/protocols/jc2mp.js b/protocols/jc2mp.js index 0f60ae46..7d94215d 100644 --- a/protocols/jc2mp.js +++ b/protocols/jc2mp.js @@ -8,12 +8,9 @@ export default class jc2mp extends gamespy3 { this.useOnlySingleSplit = true this.isJc2mp = true this.encoding = 'utf8' - } + } async run (state) { await super.run(state) - if (!state.players.length && parseInt(state.raw.numplayers)) { - state.players.setNum(parseInt(state.raw.numplayers)) - } } } diff --git a/protocols/kspdmp.js b/protocols/kspdmp.js index 8038252a..f0f9efc4 100644 --- a/protocols/kspdmp.js +++ b/protocols/kspdmp.js @@ -23,5 +23,6 @@ export default class kspdmp extends Core { state.players.push({ name }) } } + state.numplayers = state.players.length } } diff --git a/protocols/mafia2mp.js b/protocols/mafia2mp.js index 38c49fab..a716126d 100644 --- a/protocols/mafia2mp.js +++ b/protocols/mafia2mp.js @@ -18,7 +18,7 @@ export default class mafia2mp extends Core { const reader = this.reader(body) state.name = this.readString(reader) - state.raw.numplayers = this.readString(reader) + state.numplayers = parseInt(this.readString(reader)) state.maxplayers = parseInt(this.readString(reader)) state.raw.gamemode = this.readString(reader) state.password = !!reader.uint(1) diff --git a/protocols/minecraft.js b/protocols/minecraft.js index 11024f2e..107dbff0 100644 --- a/protocols/minecraft.js +++ b/protocols/minecraft.js @@ -74,19 +74,22 @@ export default class minecraft extends Core { } state.name = name } catch (e) {} + if (vanillaState.numplayers) state.numplayers = vanillaState.numplayers if (vanillaState.maxplayers) state.maxplayers = vanillaState.maxplayers if (vanillaState.players.length) state.players = vanillaState.players if (vanillaState.ping) this.registerRtt(vanillaState.ping) } if (gamespyState) { if (gamespyState.name) state.name = gamespyState.name + if (gamespyState.numplayers) state.numplayers = gamespyState.numplayers if (gamespyState.maxplayers) state.maxplayers = gamespyState.maxplayers if (gamespyState.players.length) state.players = gamespyState.players - else if (gamespyState.raw.numplayers) state.players.setNum(parseInt(gamespyState.raw.numplayers)) + else if (gamespyState.numplayers) state.numplayers = gamespyState.numplayers if (gamespyState.ping) this.registerRtt(gamespyState.ping) } if (bedrockState) { if (bedrockState.name) state.name = bedrockState.name + if (bedrockState.numplayers) state.numplayers = bedrockState.numplayers if (bedrockState.maxplayers) state.maxplayers = bedrockState.maxplayers if (bedrockState.map) state.map = bedrockState.map if (bedrockState.ping) this.registerRtt(bedrockState.ping) diff --git a/protocols/minecraftbedrock.js b/protocols/minecraftbedrock.js index cd5b0ddd..24cd216a 100644 --- a/protocols/minecraftbedrock.js +++ b/protocols/minecraftbedrock.js @@ -57,7 +57,7 @@ export default class minecraftbedrock extends Core { state.name = split.shift() state.raw.protocolVersion = split.shift() state.raw.mcVersion = split.shift() - state.players.setNum(parseInt(split.shift())) + state.numplayers = parseInt(split.shift()) state.maxplayers = parseInt(split.shift()) if (split.length) state.raw.serverId = split.shift() if (split.length) state.map = split.shift() diff --git a/protocols/minecraftvanilla.js b/protocols/minecraftvanilla.js index cadc2943..561bebb3 100644 --- a/protocols/minecraftvanilla.js +++ b/protocols/minecraftvanilla.js @@ -47,6 +47,7 @@ export default class minecraftvanilla extends Core { state.raw = json state.maxplayers = json.players.max + state.numplayers = json.players.online if (json.players.sample) { for (const player of json.players.sample) { @@ -56,18 +57,11 @@ export default class minecraftvanilla extends Core { }) } } - - // players.sample may not contain all players or no players at all, depending on how many players are online. - // Insert a dummy player object for every online player that is not listed in players.sample. - // Limit player amount to 10.000 players for performance reasons. - for (let i = state.players.length; i < Math.min(json.players.online, 10000); i++) { - state.players.push({}) - } } varIntBuffer (num) { return Buffer.from(Varint.encode(num)) - } + } buildPacket (id, data) { if (!data) data = Buffer.from([]) diff --git a/protocols/mumbleping.js b/protocols/mumbleping.js index 77e6ab77..6e7e5400 100644 --- a/protocols/mumbleping.js +++ b/protocols/mumbleping.js @@ -17,7 +17,7 @@ export default class mumbleping extends Core { state.raw.versionMinor = reader.uint(1) state.raw.versionPatch = reader.uint(1) reader.skip(8) - state.players.setNum(reader.uint(4)) + state.numplayers = reader.uint(4) state.maxplayers = reader.uint(4) state.raw.allowedbandwidth = reader.uint(4) } diff --git a/protocols/nadeo.js b/protocols/nadeo.js index a996bc8c..433f13d8 100644 --- a/protocols/nadeo.js +++ b/protocols/nadeo.js @@ -55,6 +55,7 @@ export default class nadeo extends Core { name: this.stripColors(player.Name || player.NickName) }) } + state.numplayers = state.players.length }) } diff --git a/protocols/openttd.js b/protocols/openttd.js index 418efc67..06fb32ec 100644 --- a/protocols/openttd.js +++ b/protocols/openttd.js @@ -34,7 +34,7 @@ export default class openttd extends Core { state.password = !!reader.uint(1) state.maxplayers = reader.uint(1) - state.players.setNum(reader.uint(1)) + state.numplayers = reader.uint(1) state.raw.numspectators = reader.uint(1) state.map = reader.string() state.raw.map_width = reader.uint(2) diff --git a/protocols/quake2.js b/protocols/quake2.js index afcd9eac..ba35dd92 100644 --- a/protocols/quake2.js +++ b/protocols/quake2.js @@ -82,5 +82,7 @@ export default class quake2 extends Core { if ('maxclients' in state.raw) state.maxplayers = state.raw.maxclients if ('sv_hostname' in state.raw) state.name = state.raw.sv_hostname if ('hostname' in state.raw) state.name = state.raw.hostname + if ('clients' in state.raw) state.numplayers = state.raw.clients + else state.numplayers = state.players.length + state.bots.length } } diff --git a/protocols/rfactor.js b/protocols/rfactor.js index 868a8f82..240ad786 100644 --- a/protocols/rfactor.js +++ b/protocols/rfactor.js @@ -22,7 +22,7 @@ export default class rfactor extends Core { state.raw.ping = reader.uint(2) state.raw.packedFlags = reader.uint(1) state.raw.rate = reader.uint(1) - state.players.setNum(reader.uint(1)) + state.numplayers = reader.uint(1) state.maxplayers = reader.uint(1) state.raw.bots = reader.uint(1) state.raw.packedSpecial = reader.uint(1) diff --git a/protocols/samp.js b/protocols/samp.js index 03a13f26..2b815632 100644 --- a/protocols/samp.js +++ b/protocols/samp.js @@ -18,7 +18,7 @@ export default class samp extends Core { state.raw.version = this.reader(consumed).string() } state.password = !!reader.uint(1) - state.raw.numplayers = reader.uint(2) + state.numplayers = reader.uint(2) state.maxplayers = reader.uint(2) state.name = reader.pascalString(4) state.raw.gamemode = reader.pascalString(4) @@ -39,12 +39,10 @@ export default class samp extends Core { // read players // don't even bother if > 100 players, because the server won't respond - let gotPlayerData = false - if (state.raw.numplayers < 100) { + if (state.numplayers < 100) { if (this.isVcmp) { const reader = await this.sendPacket('c', true) if (reader !== null) { - gotPlayerData = true const playerCount = reader.uint(2) for (let i = 0; i < playerCount; i++) { const player = {} @@ -55,7 +53,6 @@ export default class samp extends Core { } else { const reader = await this.sendPacket('d', true) if (reader !== null) { - gotPlayerData = true const playerCount = reader.uint(2) for (let i = 0; i < playerCount; i++) { const player = {} @@ -68,10 +65,7 @@ export default class samp extends Core { } } } - if (!gotPlayerData) { - state.players.setNum(state.raw.numplayers) - } - } + } async sendPacket (type, allowTimeout) { const outBuffer = Buffer.alloc(11) diff --git a/protocols/savage2.js b/protocols/savage2.js index 959ff73b..2578bf76 100644 --- a/protocols/savage2.js +++ b/protocols/savage2.js @@ -7,7 +7,7 @@ export default class savage2 extends Core { reader.skip(12) state.name = this.stripColorCodes(reader.string()) - state.players.setNum(reader.uint(1)) + state.numplayers = reader.uint(1) state.maxplayers = reader.uint(1) state.raw.time = reader.string() state.map = reader.string() diff --git a/protocols/starmade.js b/protocols/starmade.js index 31cb9720..079fb279 100644 --- a/protocols/starmade.js +++ b/protocols/starmade.js @@ -61,7 +61,7 @@ export default class starmade extends Core { if (typeof data[2] === 'string') state.name = data[2] if (typeof data[3] === 'string') state.raw.description = data[3] if (typeof data[4] === 'number') state.raw.startTime = data[4] - if (typeof data[5] === 'number') state.players.setNum(data[5]) + if (typeof data[5] === 'number') state.numplayers = data[5] if (typeof data[6] === 'number') state.maxplayers = data[6] } } diff --git a/protocols/teamspeak2.js b/protocols/teamspeak2.js index e70ccd0e..96e53a66 100644 --- a/protocols/teamspeak2.js +++ b/protocols/teamspeak2.js @@ -37,6 +37,7 @@ export default class teamspeak2 extends Core { }) state.players.push(player) } + state.numplayers = state.players.length } { diff --git a/protocols/teamspeak3.js b/protocols/teamspeak3.js index d079124e..3e5d3c62 100644 --- a/protocols/teamspeak3.js +++ b/protocols/teamspeak3.js @@ -16,6 +16,7 @@ export default class teamspeak3 extends Core { state.raw = data[0] if ('virtualserver_name' in state.raw) state.name = state.raw.virtualserver_name if ('virtualserver_maxclients' in state.raw) state.maxplayers = state.raw.virtualserver_maxclients + if ('virtualserver_clientsonline' in state.raw) state.numplayers = state.raw.virtualserver_clientsonline } { @@ -54,8 +55,8 @@ export default class teamspeak3 extends Core { for (const field of split) { const equals = field.indexOf('=') const key = equals === -1 ? field : field.substring(0, equals) - const value = equals === -1 - ? '' + const value = equals === -1 + ? '' : field.substring(equals + 1) .replace(/\\s/g, ' ').replace(/\\\//g, '/') unit[key] = value diff --git a/protocols/terraria.js b/protocols/terraria.js index 85dff639..f3beed8b 100644 --- a/protocols/terraria.js +++ b/protocols/terraria.js @@ -19,6 +19,6 @@ export default class terraria extends Core { state.name = json.name state.gamePort = json.port - state.raw.numplayers = json.playercount + state.numplayers = json.playercount } } diff --git a/protocols/tribes1.js b/protocols/tribes1.js index 5c34cfb0..4088ee9b 100644 --- a/protocols/tribes1.js +++ b/protocols/tribes1.js @@ -43,7 +43,7 @@ export default class tribes1 extends Core { state.raw.dedicated = !!reader.uint(1) state.raw.dropInProgress = !!reader.uint(1) state.raw.gameInProgress = !!reader.uint(1) - state.raw.playerCount = reader.uint(4) + state.numplayers = reader.uint(4) state.maxplayers = reader.uint(4) state.raw.teamPlay = reader.uint(1) state.map = this.readString(reader) @@ -127,7 +127,7 @@ export default class tribes1 extends Core { } state.players.push(playerInfo) } - } + } readFieldList (reader) { const str = this.readString(reader) @@ -137,7 +137,7 @@ export default class tribes1 extends Core { .map((a) => a.substring(1).trim().toLowerCase()) .map((a) => a === 'team name' ? 'name' : a) .map((a) => a === 'player name' ? 'name' : a) - } + } readValues (reader) { const str = this.readString(reader) @@ -145,7 +145,7 @@ export default class tribes1 extends Core { return str .split('\t') .map((a) => a.trim()) - } + } readString (reader) { return reader.pascalString(1) diff --git a/protocols/unreal2.js b/protocols/unreal2.js index 15a6ed9b..6be14061 100644 --- a/protocols/unreal2.js +++ b/protocols/unreal2.js @@ -18,7 +18,7 @@ export default class unreal2 extends Core { state.name = this.readUnrealString(reader, true) state.map = this.readUnrealString(reader, true) state.raw.gametype = this.readUnrealString(reader, true) - state.raw.numplayers = reader.uint(4) + state.numplayers = reader.uint(4) state.maxplayers = reader.uint(4) this.logger.debug(log => { log('UNREAL2 EXTRA INFO', reader.buffer.slice(reader.i)) diff --git a/protocols/valve.js b/protocols/valve.js index 352f7110..30ae02bf 100644 --- a/protocols/valve.js +++ b/protocols/valve.js @@ -67,7 +67,7 @@ export default class valve extends Core { state.raw.folder = reader.string() state.raw.game = reader.string() if (!this.goldsrcInfo) state.raw.appId = reader.uint(2) - state.raw.numplayers = reader.uint(1) + state.numplayers = reader.uint(1) state.maxplayers = reader.uint(1) if (this.goldsrcInfo) state.raw.protocol = reader.uint(1) @@ -301,7 +301,7 @@ export default class valve extends Core { state.name = rules.bat_name_s delete rules.bat_name_s if ('bat_player_count_s' in rules) { - state.raw.numplayers = parseInt(rules.bat_player_count_s) + state.numplayers = parseInt(rules.bat_player_count_s) delete rules.bat_player_count_s } if ('bat_max_players_i' in rules) { @@ -427,12 +427,11 @@ export default class valve extends Core { }) delete state.raw.players const numBots = state.raw.numbots || 0 - const numPlayers = state.raw.numplayers - numBots while (state.bots.length < numBots) { if (sortedPlayers.length) state.bots.push(sortedPlayers.pop()) else state.bots.push({}) } - while (state.players.length < numPlayers || sortedPlayers.length) { + while (state.players.length < state.numplayers - numBots || sortedPlayers.length) { if (sortedPlayers.length) state.players.push(sortedPlayers.pop()) else state.players.push({}) } diff --git a/protocols/ventrilo.js b/protocols/ventrilo.js index dbad2723..f941120f 100644 --- a/protocols/ventrilo.js +++ b/protocols/ventrilo.js @@ -17,6 +17,7 @@ export default class ventrilo extends Core { state.players.push(client) } delete state.raw.CLIENTS + state.numplayers = state.players.length if ('NAME' in state.raw) state.name = state.raw.NAME if ('MAXCLIENTS' in state.raw) state.maxplayers = state.raw.MAXCLIENTS