diff --git a/CHANGELOG.md b/CHANGELOG.md index 94de092..2938d94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### 2.0.26 +* Added support for the native minecraft bedrock protocol, since some +bedrock servers apparently do not respond to the gamespy3 protocol. + ### 2.0.25 * Support challenges in A2S_INFO (upcoming change to valve protocol) diff --git a/package.json b/package.json index e1218bf..528b39a 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ ], "main": "lib/index.js", "author": "GameDig Contributors", - "version": "2.0.25", + "version": "2.0.26", "repository": { "type": "git", "url": "https://github.com/gamedig/node-gamedig.git" diff --git a/protocols/minecraft.js b/protocols/minecraft.js index b21fefe..e5720c9 100644 --- a/protocols/minecraft.js +++ b/protocols/minecraft.js @@ -1,7 +1,16 @@ const Core = require('./core'), MinecraftVanilla = require('./minecraftvanilla'), + MinecraftBedrock = require('./minecraftbedrock'), Gamespy3 = require('./gamespy3'); +/* +Vanilla servers respond to minecraftvanilla only +Some modded vanilla servers respond to minecraftvanilla and gamespy3, or gamespy3 only +Some bedrock servers respond to gamespy3 only +Some bedrock servers respond to minecraftbedrock only +Unsure if any bedrock servers respond to gamespy3 and minecraftbedrock + */ + class Minecraft extends Core { constructor() { super(); @@ -17,25 +26,40 @@ class Minecraft extends Core { try { return await vanillaResolver.runOnceSafe(); } catch(e) {} })()); - const bedrockResolver = new Gamespy3(); - bedrockResolver.options = { + const gamespyResolver = new Gamespy3(); + gamespyResolver.options = { ...this.options, encoding: 'utf8', }; + gamespyResolver.udpSocket = this.udpSocket; + promises.push((async () => { + try { return await gamespyResolver.runOnceSafe(); } catch(e) {} + })()); + + const bedrockResolver = new MinecraftBedrock(); + bedrockResolver.options = this.options; bedrockResolver.udpSocket = this.udpSocket; promises.push((async () => { try { return await bedrockResolver.runOnceSafe(); } catch(e) {} })()); - const [ vanillaState, bedrockState ] = await Promise.all(promises); + const [ vanillaState, gamespyState, bedrockState ] = await Promise.all(promises); state.raw.vanilla = vanillaState; + state.raw.gamespy = gamespyState; state.raw.bedrock = bedrockState; - if (!vanillaState && !bedrockState) { + if (!vanillaState && !gamespyState && !bedrockState) { throw new Error('No protocols succeeded'); } + // Ordered from least worth to most worth (player names / etc) + if (bedrockState) { + if (bedrockState.name) state.name = bedrockState.name; + if (bedrockState.maxplayers) state.maxplayers = bedrockState.maxplayers; + if (bedrockState.players) state.players = bedrockState.players; + if (bedrockState.map) state.map = bedrockState.map; + } if (vanillaState) { try { let name = ''; @@ -54,10 +78,10 @@ class Minecraft extends Core { if (vanillaState.maxplayers) state.maxplayers = vanillaState.maxplayers; if (vanillaState.players) state.players = vanillaState.players; } - if (bedrockState) { - if (bedrockState.name) state.name = bedrockState.name; - if (bedrockState.maxplayers) state.maxplayers = bedrockState.maxplayers; - if (bedrockState.players) state.players = bedrockState.players; + if (gamespyState) { + if (gamespyState.name) state.name = gamespyState.name; + if (gamespyState.maxplayers) state.maxplayers = gamespyState.maxplayers; + if (gamespyState.players) state.players = gamespyState.players; } // remove dupe spaces from name state.name = state.name.replace(/\s+/g, ' '); diff --git a/protocols/minecraftbedrock.js b/protocols/minecraftbedrock.js new file mode 100644 index 0000000..0b762c5 --- /dev/null +++ b/protocols/minecraftbedrock.js @@ -0,0 +1,67 @@ +const Core = require('./core'); + +class MinecraftBedrock extends Core { + constructor() { + super(); + this.byteorder = 'be'; + } + + async run(state) { + const bufs = [ + Buffer.from([0x01]), // Message ID, ID_UNCONNECTED_PING + Buffer.from('0000000000000000', 'hex'), // Nonce / timestamp + Buffer.from('00ffff00fefefefefdfdfdfd12345678', 'hex'), // Magic + Buffer.from('0000000000000000', 'hex') // Cliend GUID + ]; + + return await this.udpSend(Buffer.concat(bufs), buffer => { + const reader = this.reader(buffer); + + const messageId = reader.uint(1); + if (messageId !== 0x1c) { + throw new Error('Invalid message id'); + } + + const nonce = reader.part(8).toString('hex'); // should match the nonce we sent + this.logger.debug('Nonce: ' + nonce); + + state.raw.guid = reader.part(8).toString('hex'); + + const magic = reader.part(16).toString('hex'); + this.logger.debug('Magic value: ' + magic); + + const statusLen = reader.uint(2); + if (reader.remaining() !== statusLen) { + throw new Error('Invalid status length: ' + reader.remaining() + ' vs ' + statusLen); + } + + const statusStr = reader.rest().toString('utf8'); + this.logger.debug('Raw status str: ' + statusStr); + + const split = statusStr.split(';'); + if (split.length < 12) { + throw new Error('Missing enough chunks in status str'); + } + + let i = 0; + state.raw.edition = split[i++]; + state.name = split[i++]; + state.raw.protocolVersion = split[i++]; + state.raw.mcVersion = split[i++]; + state.players = parseInt(split[i++]); + state.maxplayers = parseInt(split[i++]); + state.raw.serverId = split[i++]; + state.map = split[i++]; + state.raw.gameMode = split[i++]; + state.raw.nintendoOnly = !!parseInt(split[i++]); + state.raw.ipv4Port = split[i++]; + state.raw.ipv6Port = split[i++]; + + return true; + }); + + } + +} + +module.exports = MinecraftBedrock;