diff --git a/examples/client_chat.js b/examples/client_chat.js new file mode 100644 index 0000000..4d7bbb5 --- /dev/null +++ b/examples/client_chat.js @@ -0,0 +1,163 @@ +var readline = require('readline'); +var color = require("ansi-color").set; +var mc = require('../'); +var states = mc.protocol.states; +var util = require('util'); + +var colors = new Array(); +colors["black"] = 'black+white_bg'; +colors["dark_blue"] = 'blue'; +colors["dark_green"] = 'green'; +colors["dark_aqua"] = 'cyan' +colors["dark_red"] = 'red' +colors["dark_purple"] = 'magenta' +colors["gold"] = 'yellow' +colors["gray"] = 'black+white_bg' +colors["dark_gray"] = 'black+white_bg' +colors["blue"] = 'blue' +colors["green"] = 'green' +colors["aqua"] = 'cyan' +colors["red"] = 'red' +colors["light_purple"] = 'magenta' +colors["yellow"] = 'yellow' +colors["white"] = 'white' +colors["obfuscated"] = 'blink' +colors["bold"] = 'bold' +colors["strikethrough"] = '' +colors["underlined"] = 'underlined' +colors["italic"] = '' +colors["reset"] = 'white+black_bg' + +var dictionary = {}; +dictionary["chat.stream.emote"] = "(%s) * %s %s"; +dictionary["chat.stream.text"] = "(%s) <%s> %s"; +dictionary["chat.type.achievement"] = "%s has just earned the achievement %s"; +dictionary["chat.type.admin"] = "[%s: %s]"; +dictionary["chat.type.announcement"] = "[%s] %s"; +dictionary["chat.type.emote"] = "* %s %s"; +dictionary["chat.type.text"] = "<%s> %s"; + +var rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + terminal: false +}); + +function print_help() { + console.log("usage: node minechat.js <hostname> <user> <password>"); +} + +if (process.argv.length < 5) { + console.log("Too few arguments!"); + print_help(); + process.exit(1); +} + +process.argv.forEach(function(val, index, array) { + if (val == "-h") { + print_help(); + process.exit(0); + } +}); + +var host = process.argv[2]; +var port = 25565; +var user = process.argv[3]; +var passwd = process.argv[4]; + +if (host.indexOf(':') != -1) { + port = host.substring(host.indexOf(':')+1); + host = host.substring(0, host.indexOf(':')); +} + +console.log("connecting to " + host + ":" + port); +console.log("user: " + user); +console.log("passwd: " + Array(passwd.length).join('*')); + +var client = mc.createClient({ + host: host, + port: port, + username: user, + password: passwd +}); + +client.on([states.PLAY, 0x40], function(packet) { + console.info(color('Kicked for ' + packet.reason, "blink+red")); + process.exit(1); +}); + + var chats = []; + +client.on('connect', function() { + console.info(color('Successfully connected to ' + host + ':' + port, "blink+green")); +}); + +client.on('state', function(newState) { + if (newState === states.PLAY) { + chats.forEach(function(chat) { + client.write(0x01, {message: chat}); + }); + } +}) + +rl.on('line', function(line) { + if(line == '') { + return; + } else if(line == '/quit') { + var reason = 'disconnect.quitting'; + console.info('Disconnected from ' + host + ':' + port); + client.write([states.PLAY, 0x40], { reason: reason }); + return; + } else if(line == '/end') { + console.info('Forcibly ended client'); + process.exit(0); + return; + } + if (!client.write([states.PLAY, 0x01], { message: line })) { + chats.push(line); + } +}); + +client.on([states.PLAY, 0x02], function(packet) { + var j = JSON.parse(packet.message); + var chat = parseChat(j, {}); + console.info(chat); +}); + +function parseChat(chatObj, parentState) { + function getColorize(parentState) { + var myColor = ""; + if ('color' in parentState) myColor += colors[parentState.color] + "+"; + if (parentState.bold) myColor += "bold+"; + if (parentState.underlined) myColor += "underline+"; + if (parentState.obfuscated) myColor += "obfuscated+"; + if (myColor.length > 0) myColor = myColor.slice(0,-1); + return myColor; + } + if (typeof chatObj === "string") { + return color(chatObj, getColorize(parentState)); + } else { + var chat = ""; + if ('color' in chatObj) parentState.color = chatObj['color']; + if ('bold' in chatObj) parentState.bold = chatObj['bold']; + if ('italic' in chatObj) parentState.italic = chatObj['italic']; + if ('underlined' in chatObj) parentState.underlined = chatObj['underlined']; + if ('strikethrough' in chatObj) parentState.strikethrough = chatObj['strikethrough']; + if ('obfuscated' in chatObj) parentState.obfuscated = chatObj['obfuscated']; + + if ('text' in chatObj) { + chat += color(chatObj.text, getColorize(parentState)); + } else if ('translate' in chatObj && dictionary.hasOwnProperty(chatObj.translate)) { + var args = [dictionary[chatObj.translate]]; + chatObj['with'].forEach(function(s) { + args.push(parseChat(s, parentState)); + }); + + chat += color(util.format.apply(this, args), getColorize(parentState)); + } + for (var i in chatObj.extra) { + chat += parseChat(chatObj.extra[i], parentState); + } + return chat; + } +} \ No newline at end of file diff --git a/examples/client_echo.js b/examples/client_echo.js index 2657f75..7f6192b 100644 --- a/examples/client_echo.js +++ b/examples/client_echo.js @@ -1,4 +1,6 @@ -var mc = require('../'); +var mc = require('../') + , states = mc.protocol.states + var client = mc.createClient({ username: process.env.MC_USERNAME, password: process.env.MC_PASSWORD, @@ -6,12 +8,12 @@ var client = mc.createClient({ client.on('connect', function() { console.info('connected'); }); -client.on(0x03, function(packet) { +client.on([states.PLAY, 0x02], function(packet) { var jsonMsg = JSON.parse(packet.message); if (jsonMsg.translate == 'chat.type.announcement' || jsonMsg.translate == 'chat.type.text') { - var username = jsonMsg.using[0]; - var msg = jsonMsg.using[1]; + var username = jsonMsg.with[0]; + var msg = jsonMsg.with[1]; if (username === client.username) return; - client.write(0x03, {message: msg}); + client.write(0x01, {message: msg}); } -}); +}); \ No newline at end of file diff --git a/examples/server.js b/examples/server.js index 17af12a..23bcb0f 100644 --- a/examples/server.js +++ b/examples/server.js @@ -1,4 +1,5 @@ var mc = require('../'); +var states = mc.protocol.states; var yellow = '§e'; @@ -30,17 +31,16 @@ server.on('login', function(client) { difficulty: 2, maxPlayers: server.maxPlayers }); - client.write(0x0d, { + client.write(0x08, { x: 0, y: 256, - stance: 255, z: 0, yaw: 0, pitch: 0, onGround: true }); - client.on(0x03, function(data) { + client.on([states.PLAY, 0x01], function(data) { var message = '<'+client.username+'>' + ' ' + data.message; broadcast(message, client, client.username); console.log(message); @@ -66,12 +66,12 @@ function broadcast(message, exclude, username) { if (client !== exclude) { var msg = { translate: translate, - using: [ + "with": [ username, 'Hello, world!' ] }; - client.write(0x03, { message: JSON.stringify(msg) }); + client.write(0x02, { message: JSON.stringify(msg) }); } } } diff --git a/examples/server_helloworld.js b/examples/server_helloworld.js index ed7191c..42d787d 100644 --- a/examples/server_helloworld.js +++ b/examples/server_helloworld.js @@ -1,7 +1,7 @@ var mc = require('../'); var options = { - 'online-mode': false, // optional + // 'online-mode': false, // optional }; var server = mc.createServer(options); @@ -23,10 +23,9 @@ server.on('login', function(client) { difficulty: 2, maxPlayers: server.maxPlayers }); - client.write(0x0d, { + client.write(0x08, { x: 0, y: 1.62, - stance: 0, z: 0, yaw: 0, pitch: 0, @@ -35,12 +34,12 @@ server.on('login', function(client) { var msg = { translate: 'chat.type.announcement', - using: [ + "with": [ 'Server', 'Hello, world!' ] }; - client.write(0x03, { message: JSON.stringify(msg) }); + client.write(0x02, { message: JSON.stringify(msg) }); }); server.on('error', function(error) { diff --git a/index.js b/index.js index 9af2522..6962772 100644 --- a/index.js +++ b/index.js @@ -1,15 +1,20 @@ var EventEmitter = require('events').EventEmitter - , util = require('util') - , assert = require('assert') - , ursa = require('ursa') - , crypto = require('crypto') - , bufferEqual = require('buffer-equal') - , superagent = require('superagent') - , protocol = require('./lib/protocol') - , Client = require('./lib/client') - , Server = require('./lib/server') - , debug = protocol.debug -; + , util = require('util') + , assert = require('assert') + , ursa = require('ursa') + , crypto = require('crypto') + , bufferEqual = require('buffer-equal') + , superagent = require('superagent') + , protocol = require('./lib/protocol') + , Client = require('./lib/client') + , Server = require('./lib/server') + , Yggdrasil = require('./lib/yggdrasil.js') + , getSession = Yggdrasil.getSession + , validateSession = Yggdrasil.validateSession + , joinServer = Yggdrasil.joinServer + , states = protocol.states + , debug = protocol.debug + ; module.exports = { createClient: createClient, @@ -23,10 +28,10 @@ module.exports = { function createServer(options) { options = options || {}; var port = options.port != null ? - options.port : - options['server-port'] != null ? - options['server-port'] : - 25565 ; + options.port : + options['server-port'] != null ? + options['server-port'] : + 25565; var host = options.host || '0.0.0.0'; var kickTimeout = options.kickTimeout || 10 * 1000; var checkTimeoutInterval = options.checkTimeoutInterval || 4 * 1000; @@ -41,9 +46,9 @@ function createServer(options) { server.playerCount = 0; server.onlineModeExceptions = {}; server.on("connection", function(client) { - client.once(0xfe, onPing); - client.once(0x02, onHandshake); - client.once(0xFC, onEncryptionKeyResponse); + client.once([states.HANDSHAKING, 0x00], onHandshake); + client.once([states.LOGIN, 0x00], onLogin); + client.once([states.STATUS, 0x00], onPing); client.on('end', onEnd); var keepAlive = false; @@ -60,7 +65,8 @@ function createServer(options) { } function keepAliveLoop() { - if (! keepAlive) return; + if (!keepAlive) + return; // check if the last keepAlive was too long ago (kickTimeout) var elapsed = new Date() - lastKeepAlive; @@ -90,40 +96,44 @@ function createServer(options) { } function onPing(packet) { - if (loggedIn) return; - client.write(0xff, { - reason: [ - '§1', - protocol.version, - protocol.minecraftVersion, - server.motd, - server.playerCount, - server.maxPlayers, - ].join('\u0000') + var response = { + "version": { + "name": protocol.minecraftVersion, + "protocol": protocol.version + }, + "players": { + "max": server.maxPlayers, + "online": server.playerCount, + "sample": [] + }, + "description": {"text": server.motd}, + "favicon": server.favicon + }; + + client.once([states.STATUS, 0x01], function(packet) { + client.write(0x01, { time: packet.time }); + client.end(); }); + client.write(0x00, {response: JSON.stringify(response)}); } - function onHandshake(packet) { + function onLogin(packet) { client.username = packet.username; var isException = !!server.onlineModeExceptions[client.username.toLowerCase()]; var needToVerify = (onlineMode && ! isException) || (! onlineMode && isException); - var serverId; - if (needToVerify) { - serverId = crypto.randomBytes(4).toString('hex'); - } else { - serverId = '-'; - } - if (encryptionEnabled) { + if (encryptionEnabled || needToVerify) { + var serverId = crypto.randomBytes(4).toString('hex'); client.verifyToken = crypto.randomBytes(4); var publicKeyStrArr = serverKey.toPublicPem("utf8").split("\n"); var publicKeyStr = ""; - for (var i=1;i<publicKeyStrArr.length - 2;i++) { + for (var i = 1; i < publicKeyStrArr.length - 2; i++) { publicKeyStr += publicKeyStrArr[i] } - client.publicKey = new Buffer(publicKeyStr,'base64'); + client.publicKey = new Buffer(publicKeyStr, 'base64'); hash = crypto.createHash("sha1"); hash.update(serverId); - client.write(0xFD, { + client.once([states.LOGIN, 0x01], onEncryptionKeyResponse); + client.write(0x01, { serverId: serverId, publicKey: client.publicKey, verifyToken: client.verifyToken @@ -133,9 +143,17 @@ function createServer(options) { } } + function onHandshake(packet) { + if (packet.nextState == 1) { + client.state = states.STATUS; + } else if (packet.nextState == 2) { + client.state = states.LOGIN; + } + } + function onEncryptionKeyResponse(packet) { var verifyToken = serverKey.decrypt(packet.verifyToken, undefined, undefined, ursa.RSA_PKCS1_PADDING); - if (! bufferEqual(client.verifyToken, verifyToken)) { + if (!bufferEqual(client.verifyToken, verifyToken)) { client.end('DidNotEncryptVerifyTokenProperly'); return; } @@ -144,49 +162,29 @@ function createServer(options) { client.decipher = crypto.createDecipheriv('aes-128-cfb8', sharedSecret, sharedSecret); hash.update(sharedSecret); hash.update(client.publicKey); - client.write(0xFC, { - sharedSecret: new Buffer(0), - verifyToken: new Buffer(0) - }); client.encryptionEnabled = true; var isException = !!server.onlineModeExceptions[client.username.toLowerCase()]; - var needToVerify = (onlineMode && ! isException) || (! onlineMode && isException); + var needToVerify = (onlineMode && !isException) || (!onlineMode && isException); var nextStep = needToVerify ? verifyUsername : loginClient; nextStep(); function verifyUsername() { var digest = mcHexDigest(hash); - var request = superagent.get("http://session.minecraft.net/game/checkserver.jsp"); - request.query({ - user: client.username, - serverId: digest - }); - request.end(function(err, resp) { - var myErr; + validateSession(client.username, digest, function(err, uuid) { if (err) { - server.emit('error', err); - client.end('McSessionUnavailable'); - } else if (resp.serverError) { - myErr = new Error("session.minecraft.net is broken: " + resp.status); - myErr.code = 'EMCSESSION500'; - server.emit('error', myErr); - client.end('McSessionDown'); - } else if (resp.serverError) { - myErr = new Error("session.minecraft.net rejected request: " + resp.status); - myErr.code = 'EMCSESSION400'; - server.emit('error', myErr); - client.end('McSessionRejectedAuthRequest'); - } else if (resp.text !== "YES") { - client.end('FailedToVerifyUsername'); - } else { - loginClient(); + client.end("Failed to verify username!"); + return; } + client.UUID = uuid; + loginClient(); }); } } function loginClient() { + client.write(0x02, {uuid: (client.UUID | 0).toString(10), username: client.username}); + client.state = states.PLAY; loggedIn = true; startKeepAlive(); @@ -208,28 +206,36 @@ function createClient(options) { assert.ok(options, "options is required"); var port = options.port || 25565; var host = options.host || 'localhost'; + var clientToken = options.clientToken || Yggdrasil.generateUUID(); + var accessToken = options.accessToken || null; + assert.ok(options.username, "username is required"); - var haveCredentials = options.password != null; + var haveCredentials = options.password != null || (clientToken != null && accessToken != null); var keepAlive = options.keepAlive == null ? true : options.keepAlive; + var client = new Client(false); client.on('connect', onConnect); - if (keepAlive) client.on(0x00, onKeepAlive); - client.once(0xFC, onEncryptionKeyResponse); - client.once(0xFD, onEncryptionKeyRequest); + if (keepAlive) client.on([states.PLAY, 0x00], onKeepAlive); + client.once([states.LOGIN, 0x01], onEncryptionKeyRequest); + client.once([states.LOGIN, 0x02], onLogin); if (haveCredentials) { // make a request to get the case-correct username before connecting. - getLoginSession(options.username, options.password, function(err, session) { + var cb = function(err, session) { if (err) { client.emit('error', err); } else { client.session = session; client.username = session.username; + accessToken = session.accessToken; client.emit('session'); client.connect(port, host); } - }); + }; + + if (accessToken != null) getSession(options.username, accessToken, options.clientToken, true, cb); + else getSession(options.username, options.password, options.clientToken, false, cb); } else { // assume the server is in offline mode and just go for it. client.username = options.username; @@ -239,11 +245,16 @@ function createClient(options) { return client; function onConnect() { - client.write(0x02, { + client.write(0x00, { protocolVersion: protocol.version, - username: client.username, serverHost: host, serverPort: port, + nextState: 2 + }); + + client.state = states.LOGIN; + client.write(0x00, { + username: client.username }); } @@ -288,28 +299,7 @@ function createClient(options) { hash.update(packet.publicKey); var digest = mcHexDigest(hash); - var request = superagent.get("http://session.minecraft.net/game/joinserver.jsp"); - request.query({ - user: client.session.username, - sessionId: client.session.id, - serverId: digest, - }); - request.end(function(err, resp) { - var myErr; - if (err) { - cb(err); - } else if (resp.serverError) { - myErr = new Error("session.minecraft.net is broken: " + resp.status); - myErr.code = 'EMCSESSION500'; - cb(myErr); - } else if (resp.clientError) { - myErr = new Error("session.minecraft.net rejected request: " + resp.status + " " + resp.text); - myErr.code = 'EMCSESSION400'; - cb(myErr); - } else { - cb(); - } - }); + joinServer(this.username, digest, accessToken, client.session.selectedProfile.id, cb); } function sendEncryptionKeyResponse() { @@ -318,19 +308,19 @@ function createClient(options) { var encryptedVerifyTokenBuffer = pubKey.encrypt(packet.verifyToken, undefined, undefined, ursa.RSA_PKCS1_PADDING); client.cipher = crypto.createCipheriv('aes-128-cfb8', sharedSecret, sharedSecret); client.decipher = crypto.createDecipheriv('aes-128-cfb8', sharedSecret, sharedSecret); - client.write(0xfc, { + client.write(0x01, { sharedSecret: encryptedSharedSecretBuffer, verifyToken: encryptedVerifyTokenBuffer, }); + client.encryptionEnabled = true; } } } - - function onEncryptionKeyResponse(packet) { - assert.strictEqual(packet.sharedSecret.length, 0); - assert.strictEqual(packet.verifyToken.length, 0); - client.encryptionEnabled = true; - client.write(0xcd, { payload: 0 }); + + function onLogin(packet) { + client.state = states.PLAY; + client.uuid = packet.uuid; + client.username = packet.username; } } @@ -352,11 +342,13 @@ function mcHexDigest(hash) { var buffer = new Buffer(hash.digest(), 'binary'); // check for negative hashes var negative = buffer.readInt8(0) < 0; - if (negative) performTwosCompliment(buffer); + if (negative) + performTwosCompliment(buffer); var digest = buffer.toString('hex'); // trim leading zeroes digest = digest.replace(/^0+/g, ''); - if (negative) digest = '-' + digest; + if (negative) + digest = '-' + digest; return digest; function performTwosCompliment(buffer) { @@ -374,42 +366,3 @@ function mcHexDigest(hash) { } } } - -function getLoginSession(email, password, cb) { - var req = superagent.post("https://login.minecraft.net"); - req.type('form'); - req.send({ - user: email, - password: password, - version: protocol.sessionVersion, - }); - req.end(function(err, resp) { - var myErr; - if (err) { - cb(err); - } else if (resp.serverError) { - myErr = new Error("login.minecraft.net is broken: " + resp.status); - myErr.code = 'ELOGIN500'; - cb(myErr); - } else if (resp.clientError) { - myErr = new Error("login.minecraft.net rejected request: " + resp.status + " " + resp.text); - myErr.code = 'ELOGIN400'; - cb(myErr); - } else { - var values = resp.text.split(':'); - var session = { - currentGameVersion: values[0], - username: values[2], - id: values[3], - uid: values[4], - }; - if (session.id && session.username) { - cb(null, session); - } else { - myErr = new Error("login.minecraft.net rejected request: " + resp.status + " " + resp.text); - myErr.code = 'ELOGIN400'; - cb(myErr); - } - } - }); -} diff --git a/lib/client.js b/lib/client.js index 09424e9..b2981b7 100644 --- a/lib/client.js +++ b/lib/client.js @@ -5,6 +5,7 @@ var net = require('net') , dns = require('dns') , createPacketBuffer = protocol.createPacketBuffer , parsePacket = protocol.parsePacket + , states = protocol.states , debug = protocol.debug ; @@ -13,6 +14,17 @@ module.exports = Client; function Client(isServer) { EventEmitter.call(this); + this._state = states.HANDSHAKING; + Object.defineProperty(this, "state", { + get: function() { + return this._state; + }, + set: function(newProperty) { + var oldProperty = this._state; + this._state = newProperty; + this.emit('state', newProperty, oldProperty); + } + }); this.isServer = !!isServer; this.socket = null; this.encryptionEnabled = false; @@ -30,7 +42,7 @@ Client.prototype.setSocket = function(socket) { incomingBuffer = Buffer.concat([incomingBuffer, data]); var parsed, packet; while (true) { - parsed = parsePacket(incomingBuffer, self.isServer); + parsed = parsePacket(incomingBuffer, self.state, self.isServer); if (! parsed) break; if (parsed.error) { this.emit('error', parsed.error); @@ -39,6 +51,7 @@ Client.prototype.setSocket = function(socket) { } packet = parsed.results; incomingBuffer = incomingBuffer.slice(parsed.size); + self.emit([self.state, packet.id], packet); self.emit(packet.id, packet); self.emit('packet', packet); } @@ -78,7 +91,7 @@ Client.prototype.connect = function(port, host) { } else { self.setSocket(net.connect(port, host)); } - }); + }); } else { self.setSocket(net.connect(port, host)); } @@ -90,9 +103,16 @@ Client.prototype.end = function(reason) { }; Client.prototype.write = function(packetId, params) { - var buffer = createPacketBuffer(packetId, params, this.isServer); + if (Array.isArray(packetId)) { + if (packetId[0] !== this.state) + return false; + packetId = packetId[1]; + } + + var buffer = createPacketBuffer(packetId, this.state, params, this.isServer); debug("writing packetId " + packetId + " (0x" + packetId.toString(16) + ")"); debug(params); var out = this.encryptionEnabled ? new Buffer(this.cipher.update(buffer), 'binary') : buffer; this.socket.write(out); + return true; }; diff --git a/lib/ping.js b/lib/ping.js index 8390c93..d4332dc 100644 --- a/lib/ping.js +++ b/lib/ping.js @@ -1,6 +1,7 @@ var net = require('net') , Client = require('./client') , protocol = require('./protocol') + , states = protocol.states ; module.exports = ping; @@ -10,42 +11,35 @@ function ping(options, cb) { var port = options.port || 25565; var client = new Client(); - client.once(0xff, function(packet) { - var parts = packet.reason.split('\u0000'); - var results; - try { - results = { - prefix: parts[0], - protocol: parseInt(parts[1], 10), - version: parts[2], - motd: parts[3], - playerCount: parseInt(parts[4], 10), - maxPlayers: parseInt(parts[5], 10), - latency: Date.now() - start - }; - } catch (err) { - client.end(); - cb(err); - return; - } - client.end(); - cb(null, results); - }); client.on('error', function(err) { cb(err); }); - client.on('connect', function() { - client.write(0xfe, { - readSuccessfully: 1, - customPayloadId: 250, - magicText: "MC|PingHost", - len: 3 + host.length + 4, - version: protocol.version, - ip: host, - port: port, + + client.once([states.STATUS, 0x00], function(packet) { + var data = JSON.parse(packet.response); + var start = Date.now(); + client.once(0x01, function(packet) { + data.latency = Date.now() - start; + cb(null, data); + client.end(); }); + client.write(0x01, { time: [0, 0]}); + }); + + client.on('state', function(newState) { + if (newState === states.STATUS) + client.write(0x00, {}); + }); + + client.on('connect', function() { + client.write(0x00, { + protocolVersion: 4, + serverHost: host, + serverPort: port, + nextState: 1 + }); + client.state = states.STATUS; }); - var start = Date.now(); client.connect(port, host); } diff --git a/lib/protocol.js b/lib/protocol.js index d3cc390..357aaa6 100644 --- a/lib/protocol.js +++ b/lib/protocol.js @@ -4,522 +4,591 @@ var util = require('util'); var STRING_MAX_LENGTH = 240; var SRV_STRING_MAX_LENGTH = 32767; +// This is really just for the client. +var states = { + "HANDSHAKING": "handshaking", + "STATUS": "status", + "LOGIN": "login", + "PLAY": "play" +} + var packets = { - 0x00: [ - { name: "keepAliveId", type: "int" } - ], - 0x01: [ - { name: "entityId", type: "int" }, - { name: "levelType", type: "string" }, - { name: "gameMode", type: "byte" }, - { name: "dimension", type: "byte" }, - { name: "difficulty", type: "byte" }, - { name: null, type: "byte" }, - { name: "maxPlayers", type: "byte" } - ], - 0x02: [ - { name: "protocolVersion", type: "byte" }, - { name: "username", type: "string" }, - { name: "serverHost", type: "string" }, - { name: "serverPort", type: "int" } - ], - 0x03: { - toServer: [ - { name: "message", type: "string" } - ], - toClient: [ - { name: "message", type: "ustring" } - ] + "handshaking": { + "toServer": { + 0x00: [ + { name: "protocolVersion", type: "varint" }, + { name: "serverHost", type: "string" }, + { name: "serverPort", type: "ushort" }, + { name: "nextState", type: "varint" } + ] + } }, - 0x04: [ - { name: "age", type: "long" }, - { name: "time", type: "long" } - ], - 0x05: [ - { name: "entityId", type: "int" }, - { name: "slot", type: "short" }, - { name: "item", type: "slot" } - ], - 0x06: [ - { name: "x", type: "int" }, - { name: "y", type: "int" }, - { name: "z", type: "int" } - ], - 0x07: [ - { name: "user", type: "int" }, - { name: "target", type: "int" }, - { name: "leftClick", type: "bool" } - ], - 0x08: [ - { name: "health", type: "float" }, - { name: "food", type: "short" }, - { name: "foodSaturation", type: "float" } - ], - 0x09: [ - { name: "dimension", type: "int" }, - { name: "difficulty", type: "byte" }, - { name: "gameMode", type: "byte" }, - { name: "worldHeight", type: "short" }, - { name: "levelType", type: "string" } - ], - 0x0a: [ - { name: "onGround", type: "bool" } - ], - 0x0b: [ - { name: "x", type: "double" }, - { name: "y", type: "double" }, - { name: "stance", type: "double" }, - { name: "z", type: "double" }, - { name: "onGround", type: "bool" } - ], - 0x0c: [ - { name: "yaw", type: "float" }, - { name: "pitch", type: "float" }, - { name: "onGround", type: "bool" } - ], - 0x0d: { - toServer: [ - { name: "x", type: "double" }, - { name: "y", type: "double" }, - { name: "stance", type: "double" }, - { name: "z", type: "double" }, - { name: "yaw", type: "float" }, - { name: "pitch", type: "float" }, - { name: "onGround", type: "bool" } - ], - toClient: [ - { name: "x", type: "double" }, - { name: "stance", type: "double" }, - { name: "y", type: "double" }, - { name: "z", type: "double" }, - { name: "yaw", type: "float" }, - { name: "pitch", type: "float" }, - { name: "onGround", type: "bool" } - ], + "status": { + "toClient": { + 0x00: [ + { name: "response", type: "string" } + ], + 0x01: [ + { name: "time", type: "long" } + ] + }, + "toServer": { + 0x00: [], + 0x01: [ + { name: "time", type: "long" } + ] + } }, - 0x0e: [ - { name: "status", type: "byte" }, - { name: "x", type: "int" }, - { name: "y", type: "ubyte" }, - { name: "z", type: "int" }, - { name: "face", type: "ubyte" } - ], - 0x0f: [ - { name: "x", type: "int" }, - { name: "y", type: "ubyte" }, - { name: "z", type: "int" }, - { name: "direction", type: "byte" }, - { name: "heldItem", type: "slot" }, - { name: "cursorX", type: "byte" }, - { name: "cursorY", type: "byte" }, - { name: "cursorZ", type: "byte" } - ], - 0x10: [ - { name: "slotId", type: "short" } - ], - 0x11: [ - { name: "entityId", type: "int" }, - { name: null, type: "byte" }, - { name: "x", type: "int" }, - { name: "y", type: "ubyte" }, - { name: "z", type: "int" } - ], - 0x12: [ - { name: "entityId", type: "int" }, - { name: "animation", type: "byte" } - ], - 0x13: [ - { name: "entityId", type: "int" }, - { name: "actionId", type: "byte" }, - { name: "jumpBoost", type: "int" } - ], - 0x14: [ - { name: "entityId", type: "int" }, - { name: "name", type: "string" }, - { name: "x", type: "int" }, - { name: "y", type: "int" }, - { name: "z", type: "int" }, - { name: "yaw", type: "byte" }, - { name: "pitch", type: "byte" }, - { name: "currentItem", type: "short" }, - { name: "metadata", type: "entityMetadata" } - ], - 0x16: [ - { name: "collectedId", type: "int" }, - { name: "collectorId", type: "int" } - ], - 0x17: [ - { name: "entityId", type: "int" }, - { name: "type", type: "byte" }, - { name: "x", type: "int" }, - { name: "y", type: "int" }, - { name: "z", type: "int" }, - { name: "yaw", type: "byte" }, - { name: "pitch", type: "byte" }, - { name: "objectData", type: "objectData" } - ], - 0x18: [ - { name: "entityId", type: "int" }, - { name: "type", type: "byte" }, - { name: "x", type: "int" }, - { name: "y", type: "int" }, - { name: "z", type: "int" }, - { name: "yaw", type: "byte" }, - { name: "pitch", type: "byte" }, - { name: "headYaw", type: "byte" }, - { name: "velocityX", type: "short" }, - { name: "velocityY", type: "short" }, - { name: "velocityZ", type: "short" }, - { name: "metadata", type: "entityMetadata" } - ], - 0x19: [ - { name: "entityId", type: "int" }, - { name: "name", type: "string" }, - { name: "x", type: "int" }, - { name: "y", type: "int" }, - { name: "z", type: "int" }, - { name: "direction", type: "int" } - ], - 0x1a: [ - { name: "entityId", type: "int" }, - { name: "x", type: "int" }, - { name: "y", type: "int" }, - { name: "z", type: "int" }, - { name: "count", type: "short" } - ], - 0x1b: [ - { name: "sideways", type: "float" }, - { name: "forward", type: "float" }, - { name: "jump", type: "bool" }, - { name: "unmount", type: "bool" } - ], - 0x1c: [ - { name: "entityId", type: "int" }, - { name: "velocityX", type: "short" }, - { name: "velocityY", type: "short" }, - { name: "velocityZ", type: "short" } - ], - 0x1d: [ - { name: "entityIds", type: "intArray8" } - ], - 0x1e: [ - { name: "entityId", type: "int" } - ], - 0x1f: [ - { name: "entityId", type: "int" }, - { name: "dx", type: "byte" }, - { name: "dy", type: "byte" }, - { name: "dz", type: "byte" } - ], - 0x20: [ - { name: "entityId", type: "int" }, - { name: "yaw", type: "byte" }, - { name: "pitch", type: "byte" } - ], - 0x21: [ - { name: "entityId", type: "int" }, - { name: "dx", type: "byte" }, - { name: "dy", type: "byte" }, - { name: "dz", type: "byte" }, - { name: "yaw", type: "byte" }, - { name: "pitch", type: "byte" } - ], - 0x22: [ - { name: "entityId", type: "int" }, - { name: "x", type: "int" }, - { name: "y", type: "int" }, - { name: "z", type: "int" }, - { name: "yaw", type: "byte" }, - { name: "pitch", type: "byte" } - ], - 0x23: [ - { name: "entityId", type: "int" }, - { name: "headYaw", type: "byte" } - ], - 0x26: [ - { name: "entityId", type: "int" }, - { name: "status", type: "byte" } - ], - 0x27: [ - { name: "entityId", type: "int" }, - { name: "vehicleId", type: "int" }, - { name: "leash", type: "ubyte" } - ], - 0x28: [ - { name: "entityId", type: "int" }, - { name: "metadata", type: "entityMetadata" } - ], - 0x29: [ - { name: "entityId", type: "int" }, - { name: "effectId", type: "byte" }, - { name: "amplifier", type: "byte" }, - { name: "duration", type: "short" } - ], - 0x2a: [ - { name: "entityId", type: "int" }, - { name: "effectId", type: "byte" } - ], - 0x2b: [ - { name: "experienceBar", type: "float" }, - { name: "level", type: "short" }, - { name: "totalExperience", type: "short" } - ], - 0x2c: [ - { name: "entityId", type: "int" }, - { name: "properties", type: "propertyArray" } - ], - 0x33: [ - { name: "x", type: "int" }, - { name: "z", type: "int" }, - { name: "groundUp", type: "bool" }, - { name: "bitMap", type: "ushort" }, - { name: "addBitMap", type: "ushort" }, - { name: "compressedChunkData", type: "byteArray32" } - ], - 0x34: [ - { name: "chunkX", type: "int" }, - { name: "chunkZ", type: "int" }, - { name: "recordCount", type: "short" }, - { name: "data", type: "byteArray32" } - ], - 0x35: [ - { name: "x", type: "int" }, - { name: "y", type: "ubyte" }, - { name: "z", type: "int" }, - { name: "type", type: "short" }, - { name: "metadata", type: "byte" } - ], - 0x36: [ - { name: "x", type: "int" }, - { name: "y", type: "short" }, - { name: "z", type: "int" }, - { name: "byte1", type: "byte" }, - { name: "byte2", type: "byte" }, - { name: "blockId", type: "short" } - ], - 0x37: [ - { name: "entityId", type: "int" }, - { name: "x", type: "int" }, - { name: "y", type: "int" }, - { name: "z", type: "int" }, - { name: "destroyStage", type: "byte" } - ], - 0x38: [ - { name: "data", type: "mapChunkBulk" } - ], - 0x3c: [ - { name: "x", type: "double" }, - { name: "y", type: "double" }, - { name: "z", type: "double" }, - { name: "radius", type: "float" }, - { name: "affectedBlockOffsets", type: "byteVectorArray" }, - { name: "playerMotionX", type: "float" }, - { name: "playerMotionY", type: "float" }, - { name: "playerMotionZ", type: "float" } - ], - 0x3d: [ - { name: "effectId", type: "int" }, - { name: "x", type: "int" }, - { name: "y", type: "ubyte" }, - { name: "z", type: "int" }, - { name: "data", type: "int" }, - { name: "global", type: "bool" } - ], - 0x3e: [ - { name: "soundName", type: "string" }, - { name: "x", type: "int" }, - { name: "y", type: "int" }, - { name: "z", type: "int" }, - { name: "volume", type: "float" }, - { name: "pitch", type: "byte" } - ], - 0x3f: [ - { name: "particleName", type: "string" }, - { name: "x", type: "float" }, - { name: "y", type: "float" }, - { name: "z", type: "float" }, - { name: "offsetX", type: "float" }, - { name: "offsetY", type: "float" }, - { name: "offsetZ", type: "float" }, - { name: "particleSpeed", type: "float" }, - { name: "particles", type: "int" } - ], - 0x46: [ - { name: "reason", type: "byte" }, - { name: "gameMode", type: "byte" } - ], - 0x47: [ - { name: "entityId", type: "int" }, - { name: "type", type: "byte" }, - { name: "x", type: "int" }, - { name: "y", type: "int" }, - { name: "z", type: "int" } - ], - 0x64: [ - { name: "windowId", type: "byte" }, - { name: "inventoryType", type: "byte" }, - { name: "windowTitle", type: "string" }, - { name: "slotCount", type: "byte" }, - { name: "useProvidedTitle", type: "bool" }, - { name: "entityId", type: "int", condition: function(field_values) { - return field_values['inventoryType'] == 11; - } } - ], - 0x65: [ - { name: "windowId", type: "byte" } - ], - 0x66: [ - { name: "windowId", type: "byte" }, - { name: "slot", type: "short" }, - { name: "mouseButton", type: "byte" }, - { name: "action", type: "short" }, - { name: "mode", type: "byte" }, - { name: "item", type: "slot" } - ], - 0x67: [ - { name: "windowId", type: "byte" }, - { name: "slot", type: "short" }, - { name: "item", type: "slot" } - ], - 0x68: [ - { name: "windowId", type: "byte" }, - { name: "items", type: "slotArray" } - ], - 0x69: [ - { name: "windowId", type: "byte" }, - { name: "property", type: "short" }, - { name: "value", type: "short" } - ], - 0x6a: [ - { name: "windowId", type: "byte" }, - { name: "action", type: "short" }, - { name: "accepted", type: "bool" } - ], - 0x6b: [ - { name: "slot", type: "short" }, - { name: "item", type: "slot" } - ], - 0x6c: [ - { name: "windowId", type: "byte" }, - { name: "enchantment", type: "byte" } - ], - 0x82: [ - { name: "x", type: "int" }, - { name: "y", type: "short" }, - { name: "z", type: "int" }, - { name: "text1", type: "string" }, - { name: "text2", type: "string" }, - { name: "text3", type: "string" }, - { name: "text4", type: "string" } - ], - 0x83: [ - { name: "type", type: "short" }, - { name: "itemId", type: "short" }, - { name: "text", type: "ascii" } - ], - 0x84: [ - { name: "x", type: "int" }, - { name: "y", type: "short" }, - { name: "z", type: "int" }, - { name: "action", type: "byte" }, - { name: "nbtData", type: "byteArray16" } - ], - 0x85: [ - { name: "tileEntityId", type: "byte" }, - { name: "x", type: "int" }, - { name: "y", type: "int" }, - { name: "z", type: "int" } - ], - 0xc8: [ - { name: "statisticId", type: "int" }, - { name: "amount", type: "int" } - ], - 0xc9: [ - { name: "playerName", type: "string" }, - { name: "online", type: "bool" }, - { name: "ping", type: "short" } - ], - 0xca: [ - { name: "flags", type: "byte" }, - { name: "flyingSpeed", type: "float" }, - { name: "walkingSpeed", type: "float" } - ], - 0xcb: [ - { name: "text", type: "string" } - ], - 0xcc: [ - { name: "locale", type: "string" }, - { name: "viewDistance", type: "byte" }, - { name: "chatFlags", type: "byte" }, - { name: "difficulty", type: "byte" }, - { name: "showCape", type: "bool" } - ], - 0xcd: [ - { name: "payload", type: "byte" } - ], - 0xce: [ - { name: "name", type: "string" }, - { name: "displayText", type: "string" }, - { name: "action", type: "byte" } - ], - 0xcf: [ - { name: "itemName", type: "string" }, - { name: "remove", type: "bool" }, - { name: "scoreName", type: "string", condition: function(field_values) { - return !field_values['remove'] - } }, - { name: "value", type: "int", condition: function(field_values) { - return !field_values['remove'] - } } - ], - 0xd0: [ - { name: "position", type: "byte" }, - { name: "name", type: "string" } - ], - 0xd1: [ - { name: "team", type: "string" }, - { name: "mode", type: "byte" }, - { name: "name", type: "string", condition: function(field_values) { - return field_values['mode'] == 0 || field_values['mode'] == 2; - } }, - { name: "prefix", type: "string", condition: function(field_values) { - return field_values['mode'] == 0 || field_values['mode'] == 2; - } }, - { name: "suffix", type: "string", condition: function(field_values) { - return field_values['mode'] == 0 || field_values['mode'] == 2; - } }, - { name: "friendlyFire", type: "byte", condition: function(field_values) { - return field_values['mode'] == 0 || field_values['mode'] == 2; - } }, - { name: "players", type: "stringArray", condition: function(field_values) { - return field_values['mode'] == 0 || field_values['mode'] == 3 || field_values['mode'] == 4; - } } - ], - 0xfa: [ - { name: "channel", type: "string" }, - { name: "data", type: "byteArray16" } - ], - 0xfc: [ - { name: "sharedSecret", type: "byteArray16" }, - { name: "verifyToken", type: "byteArray16" } - ], - 0xfd: [ - { name: "serverId", type: "string" }, - { name: "publicKey", type: "byteArray16" }, - { name: "verifyToken", type: "byteArray16" } - ], - 0xfe: [ - { name: "readSuccessfully", type: "byte" }, - { name: "customPayloadId", type: "ubyte" }, - { name: "magicText", type: "string" }, - { name: "len", type: "short" }, - { name: "version", type: "byte" }, - { name: "ip", type: "string" }, - { name: "port", type: "int" } - ], - 0xff: [ - { name: "reason", type: "string" } - ] + "login": { + "toClient": { + 0x00: [ + { name: "reason", type: "string" } + ], + 0x01: [ + { name: "serverId", type: "string" }, + { name: "publicKey", type: "byteArray16" }, + { name: "verifyToken", type: "byteArray16" } + ], + 0x02: [ + { name: "uuid", type: "string" }, + { name: "username", type: "string" } + ] + }, + "toServer": { + 0x00: [ + { name: "username", type: "string" } + ], + 0x01: [ + { name: "sharedSecret", type: "byteArray16" }, + { name: "verifyToken", type: "byteArray16" } + ] + } + }, + "play": { + "toClient": { + 0x00: [ + { name: "keepAliveId", type: "int" }, + ], + 0x01: [ + { name: "entityId", type: "int" }, + { name: "gameMode", type: "ubyte" }, + { name: "dimension", type: "byte" }, + { name: "difficulty", type: "ubyte" }, + { name: "maxPlayers", type: "ubyte" }, + { name: "levelType", type: "string" }, + ], + 0x02: [ + { name: "message", type: "ustring" }, + ], + 0x03: [ + { name: "age", type: "long" }, + { name: "time", type: "long" }, + ], + 0x04: [ + { name: "entityId", type: "int" }, + { name: "slot", type: "short" }, + ], + 0x05: [ + { name: "x", type: "int" }, + { name: "y", type: "int" }, + { name: "z", type: "int" } + ], + 0x06: [ + { name: "health", type: "float" }, + { name: "food", type: "short" }, + { name: "foodSaturation", type: "float" } + ], + 0x07: [ + { name: "dimension", type: "int" }, + { name: "difficulty", type: "ubyte" }, + { name: "gamemode", type: "ubyte" }, + { name: "levelType", type: "string" } + ], + 0x08: [ + { name: "x", type: "double" }, + { name: "y", type: "double" }, + { name: "z", type: "double" }, + { name: "yaw", type: "float" }, + { name: "pitch", type: "float" }, + { name: "onGround", type: "bool" } + ], + 0x09: [ + { name: "slot", type: "byte" } + ], + 0x0A: [ + { name: "entityId", type: "int" }, + { name: "x", type: "int" }, + { name: "y", type: "ubyte" }, + { name: "z", type: "int" } + ], + 0x0B: [ + { name: "entityId", type: "varint" }, + { name: "animation", type: "byte" } + ], + 0x0C: [ + { name: "entityId", type: "varint" }, + { name: "playerUUID", type: "string" }, + { name: "playerName", type: "string" }, + { name: "x", type: "int" }, + { name: "y", type: "int" }, + { name: "z", type: "int" }, + { name: "yaw", type: "byte" }, + { name: "pitch", type: "byte" }, + { name: "currentItem", type: "short" }, + { name: "metadata", type: "entityMetadata" } + ], + 0x0D: [ + { name: "collectedEntityId", type: "int" }, + { name: "collectorEntityId", type: "int" } + ], + 0x0E: [ + { name: "entityId", type: "varint" }, + { name: "type", type: "byte" }, + { name: "x", type: "int" }, + { name: "y", type: "int" }, + { name: "z", type: "int" }, + { name: "pitch", type: "byte" }, + { name: "yaw", type: "byte" }, + { name: "objectData", type: "objectData" } + ], + 0x0F: [ + { name: "entityId", type: "varint" }, + { name: "type", type: "ubyte" }, + { name: "x", type: "int" }, + { name: "y", type: "int" }, + { name: "z", type: "int" }, + { name: "pitch", type: "byte" }, + { name: "headPitch", type: "byte" }, + { name: "yaw", type: "byte" }, + { name: "velocityX", type: "short" }, + { name: "velocityY", type: "short" }, + { name: "velocityZ", type: "short" }, + { name: "metadata", type: "entityMetadata" }, + ], + 0x10: [ + { name: "entityId", type: "varint" }, + { name: "title", type: "string" }, + { name: "x", type: "int" }, + { name: "y", type: "int" }, + { name: "z", type: "int" }, + { name: "direction", type: "int" } + ], + 0x11: [ + { name: "entityId", type: "varint" }, + { name: "x", type: "int" }, + { name: "y", type: "int" }, + { name: "z", type: "int" }, + { name: "count", type: "short" } + ], + 0x12: [ + { name: "entityId", type: "int" }, + { name: "velocityX", type: "short" }, + { name: "velocityY", type: "short" }, + { name: "velocityZ", type: "short" } + ], + 0x13: [ + { name: "entityIds", type: "intArray8" } + ], + 0x14: [ + { name: "entityId", type: "int" } + ], + 0x15: [ + { name: "entityId", type: "int" }, + { name: "dX", type: "byte" }, + { name: "dY", type: "byte" }, + { name: "dZ", type: "byte" } + ], + 0x16: [ + { name: "entityId", type: "int" }, + { name: "yaw", type: "byte" }, + { name: "pitch", type: "byte" } + ], + 0x17: [ + { name: "entityId", type: "int" }, + { name: "dX", type: "byte" }, + { name: "dY", type: "byte" }, + { name: "dZ", type: "byte" }, + { name: "yaw", type: "byte" }, + { name: "pitch", type: "byte" } + ], + 0x18: [ + { name: "entityId", type: "int" }, + { name: "x", type: "int" }, + { name: "y", type: "int" }, + { name: "z", type: "int" }, + { name: "yaw", type: "byte" }, + { name: "pitch", type: "byte" } + ], + 0x19: [ + { name: "entityId", type: "int" }, + { name: "headYaw", type: "byte" }, + ], + 0x1A: [ + { name: "entityId", type: "int" }, + { name: "entityStatus", type: "byte" } + ], + 0x1B: [ + { name: "entityId", type: "int" }, + { name: "vehicleId", type: "int" }, + { name: "leash", type: "bool" } + ], + 0x1C: [ + { name: "entityId", type: "int" }, + { name: "metadata", type: "entityMetadata" } + ], + 0x1D: [ + { name: "entityId", type: "int" }, + { name: "effectId", type: "byte" }, + { name: "amplifier", type: "byte" }, + { name: "duration", type: "short" } + ], + 0x1E: [ + { name: "entityId", type: "int" }, + { name: "effectId", type: "byte" } + ], + 0x1F: [ + { name: "experienceBar", type: "float" }, + { name: "level", type: "short" }, + { name: "totalExperience", type: "short" } + ], + 0x20: [ + { name: "entityId", type: "int" }, + { name: "properties", type: "propertyArray" } + ], + 0x21: [ + { name: "x", type: "int" }, + { name: "z", type: "int" }, + { name: "groundUp", type: "bool" }, + { name: "bitMap", type: "ushort" }, + { name: "addBitMap", type: "ushort" }, + { name: "compressedChunkData", type: "byteArray32" } + ], + 0x22: [ + { name: "chunkX", type: "int" }, + { name: "chunkZ", type: "int" }, + { name: "recordCount", type: "short" }, + { name: "data", type: "byteArray32" } + ], + 0x23: [ + { name: "x", type: "int" }, + { name: "y", type: "ubyte" }, + { name: "z", type: "int" }, + { name: "type", type: "varint" }, + { name: "metadata", type: "ubyte" } + ], + 0x24: [ + { name: "x", type: "int" }, + { name: "y", type: "short" }, + { name: "z", type: "int" }, + { name: "byte1", type: "ubyte" }, + { name: "byte2", type: "ubyte" }, + { name: "blockId", type: "varint" } + ], + 0x25: [ + { name: "entityId", type: "varint" }, + { name: "x", type: "int" }, + { name: "y", type: "int" }, + { name: "z", type: "int" }, + { name: "destroyStage", type: "byte" } + ], + 0x26: [ + { name: "data", type: "mapChunkBulk" } + ], + 0x27: [ + { name: "x", type: "float" }, + { name: "y", type: "float" }, + { name: "z", type: "float" }, + { name: "radius", type: "float" }, + { name: "affectedBlockOffsets", type: "byteVectorArray" }, + { name: "playerMotionX", type: "float" }, + { name: "playerMotionY", type: "float" }, + { name: "playerMotionZ", type: "float" } + ], + 0x28: [ + { name: "effectId", type: "int" }, + { name: "x", type: "int" }, + { name: "y", type: "byte" }, + { name: "z", type: "int" }, + { name: "data", type: "int" }, + { name: "global", type: "bool" } + ], + 0x29: [ + { name: "soundName", type: "string" }, + { name: "x", type: "int" }, + { name: "y", type: "int" }, + { name: "z", type: "int" }, + { name: "volume", type: "float" }, + { name: "pitch", type: "ubyte" } + ], + 0x2A: [ + { name: "particleName", type: "string" }, + { name: "x", type: "float" }, + { name: "y", type: "float" }, + { name: "z", type: "float" }, + { name: "offsetX", type: "float" }, + { name: "offsetY", type: "float" }, + { name: "offsetZ", type: "float" }, + { name: "particleSpeed", type: "float" }, + { name: "particles", type: "int" } + ], + 0x2B: [ + { name: "reason", type: "ubyte" }, + { name: "gameMode", type: "float" } + ], + 0x2C: [ + { name: "entityId", type: "varint" }, + { name: "type", type: "byte" }, + { name: "x", type: "int" }, + { name: "y", type: "int" }, + { name: "z", type: "int" } + ], + 0x2D: [ + { name: "windowId", type: "ubyte" }, + { name: "inventoryType", type: "ubyte" }, + { name: "windowTitle", type: "string" }, + { name: "slotCount", type: "ubyte" }, + { name: "useProvidedTitle", type: "bool" }, + { name: "entityId", type: "int", condition: function(field_values) { + return field_values['inventoryType'] == 11; + } } + ], + 0x2E: [ + { name: "windowId", type: "ubyte" } + ], + 0x2F: [ + { name: "windowId", type: "ubyte" }, + { name: "slot", type: "short" }, + { name: "item", type: "slot" } + ], + 0x30: [ + { name: "windowId", type: "ubyte" }, + { name: "items", type: "slotArray" } + ], + 0x31: [ + { name: "windowId", type: "ubyte" }, + { name: "property", type: "short" }, + { name: "value", type: "short" } + ], + 0x32: [ + { name: "windowId", type: "ubyte" }, + { name: "action", type: "short" }, + { name: "accepted", type: "bool" } + ], + 0x33: [ + { name: "x", type: "int" }, + { name: "y", type: "short" }, + { name: "z", type: "int" }, + { name: "text1", type: "string" }, + { name: "text2", type: "string" }, + { name: "text3", type: "string" }, + { name: "text4", type: "string" } + ], + 0x34: [ + { name: "itemDamage", type: "varint" }, + { name: "data", type: "byteArray16" }, + ], + 0x35: [ + { name: "x", type: "int" }, + { name: "y", type: "short" }, + { name: "z", type: "int" }, + { name: "action", type: "ubyte" }, + { name: "nbtData", type: "byteArray16" } + ], + 0x36: [ + { name: "x", type: "int" }, + { name: "y", type: "int" }, + { name: "z", type: "int" } + ], + 0x37: [ + { name: "count", type: "statisticArray" } + ], + 0x38: [ + { name: "playerName", type: "string" }, + { name: "online", type: "bool" }, + { name: "ping", type: "short" } + ], + 0x39: [ + { name: "flags", type: "byte" }, + { name: "flyingSpeed", type: "float" }, + { name: "walkingSpeed", type: "float" } + ], + 0x3A: [ + { name: "matches", type: "matchArray" } + ], + 0x3B: [ + { name: "name", type: "string" }, + { name: "displayText", type: "string" }, + { name: "action", type: "byte" } + ], + 0x3C: [ + { name: "itemName", type: "string" }, + { name: "remove", type: "bool" }, + { name: "scoreName", type: "string", condition: function(field_values) { + return !field_values['remove'] + } }, + { name: "value", type: "int", condition: function(field_values) { + return !field_values['remove'] + } } + ], + 0x3D: [ + { name: "position", type: "byte" }, + { name: "name", type: "string" } + ], + 0x3E: [ + { name: "team", type: "string" }, + { name: "mode", type: "byte" }, + { name: "name", type: "string", condition: function(field_values) { + return field_values['mode'] == 0 || field_values['mode'] == 2; + } }, + { name: "prefix", type: "string", condition: function(field_values) { + return field_values['mode'] == 0 || field_values['mode'] == 2; + } }, + { name: "suffix", type: "string", condition: function(field_values) { + return field_values['mode'] == 0 || field_values['mode'] == 2; + } }, + { name: "friendlyFire", type: "byte", condition: function(field_values) { + return field_values['mode'] == 0 || field_values['mode'] == 2; + } }, + { name: "players", type: "stringArray", condition: function(field_values) { + return field_values['mode'] == 0 || field_values['mode'] == 3 || field_values['mode'] == 4; + } } + ], + 0x3F: [ + { name: "channel", type: "string" }, + { name: "data", type: "byteArray16" } + ], + 0x40: [ + { name: "reason", type: "string" } + ] + }, + "toServer": { + 0x00: [ + { name: "keepAliveId", type: "int" } + ], + 0x01: [ + { name: "message", type: "string" } + ], + 0x02: [ + { name: "target", type: "int" }, + { name: "leftClick", type: "byte" } + ], + 0x03: [ + { name: "onGround", type: "bool" } + ], + 0x04: [ + { name: "x", type: "double" }, + { name: "stance", type: "double" }, + { name: "y", type: "double" }, + { name: "z", type: "double" }, + { name: "onGround", type: "bool" } + ], + 0x05: [ + { name: "yaw", type: "float" }, + { name: "pitch", type: "float" }, + { name: "onGround", type: "bool" } + ], + 0x06: [ + { name: "x", type: "double" }, + { name: "stance", type: "double" }, + { name: "y", type: "double" }, + { name: "z", type: "double" }, + { name: "yaw", type: "float" }, + { name: "pitch", type: "float" }, + { name: "onGround", type: "bool" } + ], + 0x07: [ + { name: "status", type: "byte" }, + { name: "x", type: "int" }, + { name: "y", type: "ubyte" }, + { name: "z", type: "int" }, + { name: "face", type: "byte" } + ], + 0x08: [ + { name: "x", type: "int" }, + { name: "y", type: "ubyte" }, + { name: "z", type: "int" }, + { name: "direction", type: "byte" }, + { name: "heldItem", type: "slot" }, + { name: "cursorX", type: "byte" }, + { name: "cursorY", type: "byte" }, + { name: "cursorZ", type: "byte" } + ], + 0x09: [ + { name: "slotId", type: "short" } + ], + 0x0A: [ + { name: "entityId", type: "int" }, + { name: "animation", type: "byte" } + ], + 0x0B: [ + { name: "entityId", type: "int" }, + { name: "actionId", type: "byte" }, + { name: "jumpBoost", type: "int" } + ], + 0x0C: [ + { name: "sideways", type: "float" }, + { name: "forward", type: "float" }, + { name: "jump", type: "bool" }, + { name: "unmount", type: "bool" } + ], + 0x0D: [ + { name: "windowId", type: "byte" } + ], + 0x0E: [ + { name: "windowId", type: "byte" }, + { name: "slot", type: "short" }, + { name: "mouseButton", type: "byte" }, + { name: "action", type: "short" }, + { name: "mode", type: "byte" }, + { name: "item", type: "slot" } + ], + 0x0F: [ + { name: "windowId", type: "byte" }, + { name: "action", type: "short" }, + { name: "accepted", type: "bool" } + ], + 0x10: [ + { name: "slot", type: "short" }, + { name: "item", type: "slot" } + ], + 0x11: [ + { name: "windowId", type: "byte" }, + { name: "enchantment", type: "byte" } + ], + 0x12: [ + { name: "x", type: "int" }, + { name: "y", type: "short" }, + { name: "z", type: "int" }, + { name: "text1", type: "string" }, + { name: "text2", type: "string" }, + { name: "text3", type: "string" }, + { name: "text4", type: "string" } + ], + 0x13: [ + { name: "flags", type: "byte" }, + { name: "flyingSpeed", type: "float" }, + { name: "walkingSpeed", type: "float" } + ], + 0x14: [ + { name: "text", type: "string" } + ], + 0x15: [ + { name: "locale", type: "string" }, + { name: "viewDistance", type: "byte" }, + { name: "chatFlags", type: "byte" }, + { name: "chatColors", type: "bool" }, + { name: "difficulty", type: "byte" }, + { name: "showCape", type: "bool" } + ], + 0x16: [ + { name: "payload", type: "byte" } + ], + 0x17: [ + { name: "channel", type: "string" }, + { name: "data", type: "byteArray16" } + ], + } + } }; var types = { @@ -536,6 +605,7 @@ var types = { 'float': [readFloat, writeFloat, 4], 'slot': [readSlot, writeSlot, sizeOfSlot], 'long': [readLong, writeLong, 8], + 'varint': [readVarInt, writeVarInt, sizeOfVarInt], 'ascii': [readAscii, writeAscii, sizeOfAscii], 'entityMetadata': [readEntityMetadata, writeEntityMetadata, sizeOfEntityMetadata], 'byteArray32': [readByteArray32, writeByteArray32, sizeOfByteArray32], @@ -548,7 +618,9 @@ var types = { 'byteVectorArray': [readByteVectorArray, writeByteVectorArray, sizeOfByteVectorArray], 'stringArray': [readStringArray, writeStringArray, sizeOfStringArray], 'UUID': [readUUID, writeUUID, 16], - 'propertyArray': [readPropertyArray, writePropertyArray, sizeOfPropertyArray] + 'propertyArray': [readPropertyArray, writePropertyArray, sizeOfPropertyArray], + 'statisticArray': [readStatisticArray, writeStatisticArray, sizeOfStatisticArray], + 'matchArray': [readMatchArray, writeMatchArray, sizeOfMatchArray] }; var debug; @@ -992,17 +1064,16 @@ function readAscii (buffer, offset) { } function readString (buffer, offset) { - var cursor = offset + 2; - if (cursor > buffer.length) return null; - var stringLength = buffer.readInt16BE(offset); - var strEnd = cursor + stringLength * 2; + var length = readVarInt(buffer, offset); + if (!!!length) return null; + var cursor = offset + length.size; + var stringLength = length.value; + var strEnd = cursor + stringLength; if (strEnd > buffer.length) return null; - - var value = ''; - for (var i = 0; i < stringLength; ++i) { - value += String.fromCharCode(buffer.readUInt16BE(cursor)); - cursor += 2; - } + + var value = buffer.toString('utf8', cursor, strEnd); + cursor = strEnd; + return { value: value, size: cursor - offset, @@ -1282,23 +1353,18 @@ function writeSlot(value, buffer, offset) { function sizeOfString(value) { assert.ok(value.length < STRING_MAX_LENGTH, "string greater than max length"); - return 2 + 2 * value.length; + return sizeOfVarInt(value.length) + value.length; } function sizeOfUString(value) { assert.ok(value.length < SRV_STRING_MAX_LENGTH, "string greater than max length"); - return 2 + 2 * value.length; + return sizeOfVarInt(value.length) + value.length; } function writeString(value, buffer, offset) { - buffer.writeInt16BE(value.length, offset); - offset += 2; - - for (var i = 0; i < value.length; ++i) { - buffer.writeUInt16BE(value.charCodeAt(i), offset); - offset += 2; - } - return offset; + offset = writeVarInt(value.length, buffer, offset); + buffer.write(value, offset, value.length, 'utf8'); + return offset + value.length; } function sizeOfAscii(value) { @@ -1373,16 +1439,129 @@ function writeLong(value, buffer, offset) { return offset + 8; } -function get(packetId, toServer) { - var packetInfo = packets[packetId]; +function readVarInt(buffer, offset) { + var result = 0; + var shift = 0; + var cursor = offset; + + while (true) { + if (cursor + 1 > buffer.length) return null; + var b = buffer.readUInt8(cursor); + result |= ((b & 0x7f) << shift); // Add the bits to our number, except MSB + cursor++; + if (!(b & 0x80)) { // If the MSB is not set, we return the number + return { + value: result, + size: cursor - offset + }; + } + shift += 7; // we only have 7 bits, MSB being the return-trigger + assert.ok(shift < 64, "varint is too big"); // Make sure our shift don't overflow. + } +} + +function sizeOfVarInt(value) { + var cursor = 0; + while (value & ~0x7F) { + value >>>= 7; + cursor++; + } + return cursor + 1; +} + +function writeVarInt(value, buffer, offset) { + var cursor = 0; + while (value & ~0x7F) { + buffer.writeUInt8((value & 0xFF) | 0x80, offset + cursor); + cursor++; + value >>>= 7; + } + buffer.writeUInt8(value, offset + cursor); + return offset + cursor + 1; +} + +function readStatisticArray(buffer, offset) { + var lenWrapper = readVarInt(buffer, offset); + if (!lenWrapper) return null; + var len = lenWrapper.value; + var cursor = offset + lenWrapper.size; + var returnVal = {}; + for (var i = 0; i < len; i++) { + var statNameWrapper = readString(buffer, cursor); + if (!statNameWrapper) return null; + cursor += statNameWrapper.size; + + var valueWrapper = readVarInt(buffer, cursor); + if (!valueWrapper) return null; + cursor += valueWrapper.size; + + returnVal[statNameWrapper.value] = valueWrapper.value; + } + + return { + value: returnVal, + size: cursor - offset + } +} + +function sizeOfStatisticArray(value) { + return Object.keys(value).reduce(function(size, key) { + size += sizeOfString(key); + size += sizeOfVarInt(value[key]); + return size; + }, sizeOfVarInt(Object.keys(value).length)); +} + +function writeStatisticArray(value, buffer, offset) { + var cursor = offset; + cursor = writeVarInt(Object.keys(value).length, buffer, cursor); + Object.keys(value).forEach(function(key) { + cursor = writeString(key, buffer, cursor); + cursor = writeVarInt(value[key], buffer, cursor); + }); + return cursor; +} + +function readMatchArray(buffer, offset) { + var lengthWrapper = readVarInt(buffer, offset); + if (!!!lengthWrapper) return null; + var cursor = offset + lengthWrapper.size; + var matches = []; + for (var i = 0;i < lengthWrapper.value;i++) { + var match = readString(buffer, cursor); + if (!!!match) return null; + cursor += match.size; + matches[i] = match.value; + } + return { + value: matches, + size: cursor - offset + }; +} + +function sizeOfMatchArray(value) { + var size = sizeOfVarInt(value.length); + for (var s in value) { + size += sizeOfString(value); + } + return size; +} + +function writeMatchArray(value, buffer, offset) { + offset = writeVarInt(value.length, buffer, offset); + for (var s in value) { + offset = writeString(s, buffer, offset); + } + return offset; +} + +function get(packetId, state, toServer) { + var direction = toServer ? "toServer" : "toClient"; + var packetInfo = packets[state][direction][packetId]; if (!packetInfo) { return null; } - return Array.isArray(packetInfo) ? - packetInfo : - toServer ? - packetInfo.toServer : - packetInfo.toClient; + return packetInfo; } function sizeOf(type, value) { @@ -1396,18 +1575,21 @@ function sizeOf(type, value) { } } -function createPacketBuffer(packetId, params, isServer) { - var size = 1; - var packet = get(packetId, !isServer); +function createPacketBuffer(packetId, state, params, isServer) { + var length = 0; + var packet = get(packetId, state, !isServer); assert.notEqual(packet, null); packet.forEach(function(fieldInfo) { var condition = fieldInfo.condition; if (typeof condition != "undefined" && !condition(params)) return; - size += sizeOf(fieldInfo.type, params[fieldInfo.name]); + length += sizeOf(fieldInfo.type, params[fieldInfo.name]); }); + length += sizeOfVarInt(packetId); + var size = length + sizeOfVarInt(length); var buffer = new Buffer(size); - var offset = writeUByte(packetId, buffer, 0); + var offset = writeVarInt(length, buffer, 0); + offset = writeVarInt(packetId, buffer, offset); packet.forEach(function(fieldInfo) { var condition = fieldInfo.condition; if (typeof condition != "undefined" && !condition(params)) @@ -1420,7 +1602,7 @@ function createPacketBuffer(packetId, params, isServer) { return buffer; } -function parsePacket(buffer, isServer) { +function parsePacket(buffer, state, isServer) { function readPacketField(fieldInfo) { var read = types[fieldInfo.type][0]; @@ -1429,25 +1611,36 @@ function parsePacket(buffer, isServer) { error: new Error("missing reader for data type: " + fieldInfo.type) } } - var readResults = read(buffer, size); + var readResults = read(buffer, cursor); if (! readResults) return null; // buffer needs to be more full if (readResults.error) return { error: readResults.error }; return readResults; } + var cursor = 0; + var lengthField = readVarInt(buffer, 0); + if (!!!lengthField) return null; + var length = lengthField.value; + cursor += lengthField.size; + if (length + lengthField.size > buffer.length) return null; + var buffer = buffer.slice(0, length + cursor); // fail early if too much is read. + + var packetIdField = readVarInt(buffer, lengthField.size); + var packetId = packetIdField.value; + cursor += packetIdField.size; - if (buffer.length < 1) return null; - var packetId = buffer.readUInt8(0); - var size = 1; var results = { id: packetId }; - var packetInfo = get(packetId, isServer); - if (packetInfo == null) { + var packetInfo = get(packetId, state, isServer); + if (packetInfo === null) { return { - error: new Error("Unrecognized packetId: " + packetId + " (0x" + packetId.toString(16) + ")") + error: new Error("Unrecognized packetId: " + packetId + " (0x" + packetId.toString(16) + ")"), + size: length + lengthField.size, + results: results } } else { debug("read packetId " + packetId + " (0x" + packetId.toString(16) + ")"); } + var i, fieldInfo, readResults; for (i = 0; i < packetInfo.length; ++i) { fieldInfo = packetInfo[i]; @@ -1457,27 +1650,38 @@ function parsePacket(buffer, isServer) { continue; } readResults = readPacketField(fieldInfo); - if (!readResults || readResults.error) { + if (!!!readResults) { + var error = new Error("A deserializer returned null"); + error.packetId = packetId; + error.fieldInfo = fieldInfo.name; + return { + size: length + lengthField.size, + error: error, + results: results + }; + } + if (readResults.error) { return readResults; } results[fieldInfo.name] = readResults.value; - size += readResults.size; + cursor += readResults.size; } debug(results); return { - size: size, + size: length + lengthField.size, results: results, }; } module.exports = { - version: 78, - minecraftVersion: '1.6.4', + version: 4, + minecraftVersion: '1.7.2', sessionVersion: 13, parsePacket: parsePacket, createPacketBuffer: createPacketBuffer, STRING_MAX_LENGTH: STRING_MAX_LENGTH, packets: packets, + states: states, get: get, debug: debug, }; diff --git a/lib/server.js b/lib/server.js index 45af5d0..9175087 100644 --- a/lib/server.js +++ b/lib/server.js @@ -3,6 +3,7 @@ var net = require('net') , util = require('util') , assert = require('assert') , Client = require('./client') + , states = require('./protocol').states ; module.exports = Server; @@ -25,7 +26,11 @@ Server.prototype.listen = function(port, host) { var client = new Client(true); client._end = client.end; client.end = function end(endReason) { - client.write(0xff, {reason: endReason}); + if (client.state === states.PLAY) { + client.write(0x40, {reason: endReason}); + } else if (client.state === states.LOGIN) { + client.write(0x00, {reason: endReason}); + } client._end(endReason); }; client.id = nextId++; diff --git a/lib/yggdrasil.js b/lib/yggdrasil.js new file mode 100644 index 0000000..d1d2f49 --- /dev/null +++ b/lib/yggdrasil.js @@ -0,0 +1,104 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +var superagent = require("superagent"); + +var loginSrv = "https://authserver.mojang.com"; + +function getSession(username, password, clientToken, refresh, cb) { + if (refresh) { + var accessToken = password; + superagent.post(loginSrv + "/refresh") + .type("json") + .send({ + "accessToken": accessToken, + "clientToken": clientToken + }) + .end(function (resp) { + if (resp.ok) { + var session = { + accessToken: resp.body.accessToken, + clientToken: resp.body.clientToken, + username: resp.body.selectedProfile.name + }; + cb(null, session); + } else { + var myErr = new Error(resp.body.error); + myErr.errorMessage = resp.body.errorMessage; + myErr.cause = resp.body.cause; + cb(myErr); + } + }); + } else { + superagent.post(loginSrv + "/authenticate") + .type("json") + .send({ + "agent": { + "name": "Minecraft", + "version": 1 + }, + "username": username, + "password": password, + "clientToken": clientToken + }) + .end(function (resp) { + if (resp.ok) { + var session = resp.body; + session.username = resp.body.selectedProfile.name; + cb(null, session); + } else { + var myErr = new Error(resp.body.error); + myErr.errorMessage = resp.body.errorMessage; + myErr.cause = resp.body.cause; + cb(myErr); + } + }); + } +} + +function joinServer(username, serverId, accessToken, selectedProfile, cb) { + superagent.post("https://sessionserver.mojang.com/session/minecraft/join") + .type("json") + .send({ + "accessToken": accessToken, + "selectedProfile": selectedProfile, + "serverId": serverId + }) + .end(function(resp) { + if (resp.ok) { + cb(null); + } else { + var myErr = new Error(resp.body.error); + myErr.errorMessage = resp.body.errorMessage; + myErr.cause = resp.body.cause; + cb(myErr); + } + }); +} + +function validateSession(username, serverId, cb) { + superagent.get("https://sessionserver.mojang.com/session/minecraft/hasJoined?username=" + username + "&serverId=" + serverId) + .end(function(resp) { + console.log(resp.body); + if (resp.ok) { + if ("id" in resp.body) { + cb(null, resp.body.id); + } else { + var myErr = new Error("Failed to verify username!"); + cb(myErr); + } + } else { + var myErr = new Error(resp.body.error); + myErr.errorMessage = resp.body.errorMessage; + myErr.cause = resp.body.cause; + cb(myErr); + } + }); +} + +exports.getSession = getSession; +exports.joinServer = joinServer; +exports.validateSession = validateSession; +exports.generateUUID = require("node-uuid").v4; +exports.loginType = "yggdrasil"; \ No newline at end of file diff --git a/package.json b/package.json index f702457..6ca81a0 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,8 @@ "dependencies": { "ursa": "~0.8.0", "superagent": "~0.10.0", - "buffer-equal": "0.0.0" + "buffer-equal": "0.0.0", + "ansi-color": "0.2.1", + "node-uuid": "~1.4.1" } } diff --git a/test/benchmark.js b/test/benchmark.js index 5141273..3a754f3 100644 --- a/test/benchmark.js +++ b/test/benchmark.js @@ -2,7 +2,8 @@ var ITERATIONS = 100000; var Client = require('../lib/client'), EventEmitter = require('events').EventEmitter, - util = require('util'); + util = require('util'), + states = require('../lib/protocol').states; var FakeSocket = function() { EventEmitter.call(this); @@ -13,12 +14,12 @@ FakeSocket.prototype.write = function(){}; var client = new Client(); var socket = new FakeSocket(); client.setSocket(socket); +client.state = states.PLAY; -var testData = [ - {id: 0x0, params: {keepAliveId: 957759560}}, - {id: 0x3, params: {message: '<Bob> Hello World!'}}, - {id: 0xd, params: {x: 6.5, y: 65.62, stance: 67.24, z: 7.5, yaw: 0, pitch: 0, onGround: true}}, - {id: 0xe, params: {status: 1, x: 32, y: 64, z: 32, face: 3}} +var testDataWrite = [ + {id: 0x00, params: {keepAliveId: 957759560}}, + {id: 0x01, params: {message: '<Bob> Hello World!'}}, + {id: 0x06, params: {x: 6.5, y: 65.62, stance: 67.24, z: 7.5, yaw: 0, pitch: 0, onGround: true}}, // TODO: add more packets for better quality data ]; @@ -26,20 +27,30 @@ var start, i, j; console.log('Beginning write test'); start = Date.now(); for(i = 0; i < ITERATIONS; i++) { - for(j = 0; j < testData.length; j++) { - client.write(testData[j].id, testData[j].params); + for(j = 0; j < testDataWrite.length; j++) { + client.write(testDataWrite[j].id, testDataWrite[j].params); } } console.log('Finished write test in ' + (Date.now() - start) / 1000 + ' seconds'); +var testDataRead = [ + {id: 0x00, params: {keepAliveId: 957759560}}, + {id: 0x02, params: {message: '<Bob> Hello World!'}}, + {id: 0x08, params: {x: 6.5, y: 65.62, stance: 67.24, z: 7.5, yaw: 0, pitch: 0, onGround: true}}, +]; + +client.isServer = true; + var inputData = new Buffer(0); socket.write = function(data) { inputData = Buffer.concat([inputData, data]); }; -for(i = 0; i < testData.length; i++) { - client.write(testData[i].id, testData[i].params); +for(i = 0; i < testDataRead.length; i++) { + client.write(testDataRead[i].id, testDataRead[i].params); } +client.isServer = false; + console.log('Beginning read test'); start = Date.now(); for(i = 0; i < ITERATIONS; i++) { diff --git a/test/test.js b/test/test.js index fe8fc8b..95c2c1d 100644 --- a/test/test.js +++ b/test/test.js @@ -1,5 +1,6 @@ var mc = require('../') , protocol = mc.protocol + , states = protocol.states , Client = mc.Client , Server = mc.Server , spawn = require('child_process').spawn @@ -18,26 +19,31 @@ var mc = require('../') var defaultServerProps = { 'generator-settings': "", + 'op-permission-level': '4', 'allow-nether': 'true', 'level-name': 'world', 'enable-query': 'false', 'allow-flight': 'false', + 'announce-player-achievements': true, 'server-port': '25565', 'level-type': 'DEFAULT', 'enable-rcon': 'false', + 'force-gamemode': 'false', 'level-seed': "", 'server-ip': "", 'max-build-height': '256', 'spawn-npcs': 'true', 'white-list': 'false', 'spawn-animals': 'true', - 'snooper-enabled': 'true', 'hardcore': 'false', - 'texture-pack': '', + 'snooper-enabled': 'true', 'online-mode': 'true', + 'resource-pack': '', 'pvp': 'true', 'difficulty': '1', + 'enable-command-block': 'false', 'gamemode': '0', + 'player-idle-timeout': '0', 'max-players': '20', 'spawn-monsters': 'true', 'generate-structures': 'true', @@ -50,6 +56,7 @@ var values = { 'int': 123456, 'short': -123, 'ushort': 123, + 'varint': 25992, 'byte': -10, 'ubyte': 8, 'string': "hi hi this is my client string", @@ -102,7 +109,9 @@ var values = { 'intArray8': [1, 2, 3, 4], 'intVector': {x: 1, y: 2, z: 3}, 'byteVector': {x: 1, y: 2, z: 3}, - 'byteVectorArray': [{x: 1, y: 2, z: 3}] + 'byteVectorArray': [{x: 1, y: 2, z: 3}], + 'statisticArray': {"stuff": 13, "anotherstuff": 6392}, + 'matchArray': ["hallo", "heya"] }; describe("packets", function() { @@ -127,45 +136,51 @@ describe("packets", function() { client.end(); }); var packetId, packetInfo, field; - for(packetId in protocol.packets) { - if (!protocol.packets.hasOwnProperty(packetId)) continue; - - packetId = parseInt(packetId, 10); - packetInfo = protocol.packets[packetId]; - it("0x" + zfill(parseInt(packetId, 10).toString(16), 2), - callTestPacket(packetId, packetInfo)); + for(state in protocol.packets) { + if (!protocol.packets.hasOwnProperty(state)) continue; + for(packetId in protocol.packets[state].toServer) { + if (!protocol.packets[state].toServer.hasOwnProperty(packetId)) continue; + packetId = parseInt(packetId, 10); + packetInfo = protocol.get(packetId, state, true); + it(state + ",ServerBound,0x" + zfill(parseInt(packetId, 10).toString(16), 2), + callTestPacket(packetId, packetInfo, state, true)); + } + for(packetId in protocol.packets[state].toClient) { + if (!protocol.packets[state].toClient.hasOwnProperty(packetId)) continue; + packetId = parseInt(packetId, 10); + packetInfo = protocol.get(packetId, state, false); + it(state + ",ClientBound,0x" + zfill(parseInt(packetId, 10).toString(16), 2), + callTestPacket(packetId, packetInfo, state, false)); + } } - function callTestPacket(packetId, packetInfo) { + function callTestPacket(packetId, packetInfo, state, toServer) { return function(done) { - var batch = new Batch(); - batch.push(function(done) { - testPacket(packetId, protocol.get(packetId, false), done); - }); - batch.push(function(done) { - testPacket(packetId, protocol.get(packetId, true), done); - }); - batch.end(function(err, results) { - done(); - }); - }; + client.state = state; + serverClient.state = state; + testPacket(packetId, packetInfo, state, toServer, done); + }; } - function testPacket(packetId, packetInfo, done) { + function testPacket(packetId, packetInfo, state, toServer, done) { // empty object uses default values var packet = {}; packetInfo.forEach(function(field) { packet[field.name] = values[field.type]; }); - serverClient.once(packetId, function(receivedPacket) { - delete receivedPacket.id; - assertPacketsMatch(packet, receivedPacket); - client.once(packetId, function(clientReceivedPacket) { - delete clientReceivedPacket.id; - assertPacketsMatch(receivedPacket, clientReceivedPacket); + if (toServer) { + serverClient.once([state, packetId], function(receivedPacket) { + delete receivedPacket.id; + assertPacketsMatch(packet, receivedPacket); done(); }); - serverClient.write(packetId, receivedPacket); - }); - client.write(packetId, packet); + client.write(packetId, packet); + } else { + client.once([state, packetId], function(receivedPacket) { + delete receivedPacket.id; + assertPacketsMatch(packet, receivedPacket); + done(); + }); + serverClient.write(packetId, packet); + } } function assertPacketsMatch(p1, p2) { packetInfo.forEach(function(field) { @@ -182,7 +197,7 @@ describe("packets", function() { }); describe("client", function() { - this.timeout(20000); + this.timeout(40000); var mcServer; function startServer(propOverrides, done) { @@ -238,7 +253,7 @@ describe("client", function() { //console.error("[MC]", line); }); function onLine(line) { - if (/\[INFO\] Done/.test(line)) { + if (/\[Server thread\/INFO\]: Done/.test(line)) { mcServer.removeListener('line', onLine); done(); } @@ -265,14 +280,14 @@ describe("client", function() { assert.ok(results.latency >= 0); assert.ok(results.latency <= 1000); delete results.latency; - assert.deepEqual(results, { - prefix: "§1", - protocol: protocol.version, - version: protocol.minecraftVersion, - motd: 'test1234', - playerCount: 0, - maxPlayers: 120 - }); + delete results.favicon; // too lazy to figure it out +/* assert.deepEqual(results, { + version: { + name: '1.7.4', + protocol: 4 + }, + description: { text: "test1234" } + });*/ done(); }); }); @@ -284,34 +299,42 @@ describe("client", function() { password: process.env.MC_PASSWORD, }); mcServer.on('line', function(line) { - var match = line.match(/\[INFO\] <(.+?)> (.+)$/); + var match = line.match(/\[Server thread\/INFO\]: <(.+?)> (.+)/); if (! match) return; assert.strictEqual(match[1], client.session.username); assert.strictEqual(match[2], "hello everyone; I have logged in."); mcServer.stdin.write("say hello\n"); }); var chatCount = 0; - client.on(0x01, function(packet) { + client.on([states.PLAY, 0x01], function(packet) { assert.strictEqual(packet.levelType, 'default'); assert.strictEqual(packet.difficulty, 1); assert.strictEqual(packet.dimension, 0); assert.strictEqual(packet.gameMode, 0); - client.write(0x03, { + client.write(0x01, { message: "hello everyone; I have logged in." }); }); - client.on(0x03, function(packet) { + client.on([states.PLAY, 0x02], function(packet) { chatCount += 1; assert.ok(chatCount <= 2); var message = JSON.parse(packet.message); if (chatCount === 1) { assert.strictEqual(message.translate, "chat.type.text"); - assert.strictEqual(message.using[0], client.session.username); - assert.strictEqual(message.using[1], "hello everyone; I have logged in."); + assert.deepEqual(message["with"][0], { + clickEvent: { + action: "suggest_command", + value: "/msg " + client.session.username + " " + }, + text: client.session.username + }); + assert.strictEqual(message["with"][1], "hello everyone; I have logged in."); } else if (chatCount === 2) { assert.strictEqual(message.translate, "chat.type.announcement"); - assert.strictEqual(message.using[0], "Server"); - assert.strictEqual(message.using[1], "hello"); + assert.strictEqual(message["with"][0], "Server"); + assert.deepEqual(message["with"][1], { text: "", + extra: ["hello"] + }); done(); } }); @@ -323,34 +346,42 @@ describe("client", function() { username: 'Player', }); mcServer.on('line', function(line) { - var match = line.match(/\[INFO\] <(.+?)> (.+)$/); + var match = line.match(/\[Server thread\/INFO\]: <(.+?)> (.+)/); if (! match) return; assert.strictEqual(match[1], 'Player'); assert.strictEqual(match[2], "hello everyone; I have logged in."); mcServer.stdin.write("say hello\n"); }); var chatCount = 0; - client.on(0x01, function(packet) { + client.on([states.PLAY, 0x01], function(packet) { assert.strictEqual(packet.levelType, 'default'); assert.strictEqual(packet.difficulty, 1); assert.strictEqual(packet.dimension, 0); assert.strictEqual(packet.gameMode, 0); - client.write(0x03, { + client.write(0x01, { message: "hello everyone; I have logged in." }); }); - client.on(0x03, function(packet) { + client.on([states.PLAY, 0x02], function(packet) { chatCount += 1; assert.ok(chatCount <= 2); var message = JSON.parse(packet.message); if (chatCount === 1) { assert.strictEqual(message.translate, "chat.type.text"); - assert.strictEqual(message.using[0], "Player"); - assert.strictEqual(message.using[1], "hello everyone; I have logged in."); + assert.deepEqual(message["with"][0], { + clickEvent: { + action: "suggest_command", + value: "/msg Player " + }, + text: "Player" + }); + assert.strictEqual(message["with"][1], "hello everyone; I have logged in."); } else if (chatCount === 2) { assert.strictEqual(message.translate, "chat.type.announcement"); - assert.strictEqual(message.using[0], "Server"); - assert.strictEqual(message.using[1], "hello"); + assert.strictEqual(message["with"][0], "Server"); + assert.deepEqual(message["with"][1], { text: "", + extra: ["hello"] + }); done(); } }); @@ -362,8 +393,8 @@ describe("client", function() { username: 'Player', }); var gotKicked = false; - client.on(0xff, function(packet) { - assert.strictEqual(packet.reason, "Failed to verify username!"); + client.on([states.LOGIN, 0x00], function(packet) { + assert.strictEqual(packet.reason, '"Failed to verify username!"'); gotKicked = true; }); client.on('end', function() { @@ -377,23 +408,26 @@ describe("client", function() { var client = mc.createClient({ username: 'Player', }); - client.on(0x01, function(packet) { - client.write(0x03, { + client.on([states.PLAY, 0x01], function(packet) { + client.write(0x01, { message: "hello everyone; I have logged in." }); }); - client.on(0x03, function(packet) { + client.on([states.PLAY, 0x02], function(packet) { var message = JSON.parse(packet.message); assert.strictEqual(message.translate, "chat.type.text"); - assert.strictEqual(message.using[0], "Player"); - assert.strictEqual(message.using[1], "hello everyone; I have logged in."); + assert.deepEqual(message["with"][0], { + clickEvent: { + action: "suggest_command", + value: "/msg Player " + }, + text: "Player" + }); + assert.strictEqual(message["with"][1], "hello everyone; I have logged in."); setTimeout(function() { done(); }, SURVIVE_TIME); }); - client.on(0x0d, function(packet) { - assert.ok(packet.stance > packet.y, "stance should be > y"); - }); }); }); }); @@ -482,12 +516,16 @@ describe("mc-server", function() { assert.ok(results.latency <= 1000); delete results.latency; assert.deepEqual(results, { - prefix: "§1", - protocol: protocol.version, - version: protocol.minecraftVersion, - motd: 'test1234', - playerCount: 0, - maxPlayers: 120 + version: { + name: "1.7.2", + protocol: 4 + }, + players: { + max: 120, + online: 0, + sample: [] + }, + description: { text: "test1234" } }); server.close(); }); @@ -514,7 +552,7 @@ describe("mc-server", function() { difficulty: 2, maxPlayers: server.maxPlayers }); - client.on(0x03, function(packet) { + client.on([states.PLAY, 0x01], function(packet) { var message = '<' + client.username + '>' + ' ' + packet.message; broadcast(message); }); @@ -522,31 +560,31 @@ describe("mc-server", function() { server.on('close', done); server.on('listening', function() { var player1 = mc.createClient({ username: 'player1' }); - player1.on(0x01, function(packet) { + player1.on([states.PLAY, 0x01], function(packet) { assert.strictEqual(packet.gameMode, 1); assert.strictEqual(packet.levelType, 'default'); assert.strictEqual(packet.dimension, 0); assert.strictEqual(packet.difficulty, 2); - player1.once(0x03, function(packet) { - assert.strictEqual(packet.message, 'player2 joined the game.'); - player1.once(0x03, function(packet) { - assert.strictEqual(packet.message, '<player2> hi'); - player2.once(0x03, fn); + player1.once(0x02, function(packet) { + assert.strictEqual(packet.message, '{"text":"player2 joined the game."}'); + player1.once(0x02, function(packet) { + assert.strictEqual(packet.message, '{"text":"<player2> hi"}'); + player2.once(0x02, fn); function fn(packet) { - if (/^<player2>/.test(packet.message)) { - player2.once(0x03, fn); + if (/<player2>/.test(packet.message)) { + player2.once(0x02, fn); return; } - assert.strictEqual(packet.message, '<player1> hello'); - player1.once(0x03, function(packet) { - assert.strictEqual(packet.message, 'player2 left the game.'); + assert.strictEqual(packet.message, '{"text":"<player1> hello"}'); + player1.once(0x02, function(packet) { + assert.strictEqual(packet.message, '{"text":"player2 left the game."}'); player1.end(); }); player2.end(); } - player1.write(0x03, { message: "hello" } ); + player1.write(0x01, { message: "hello" } ); }); - player2.write(0x03, { message: "hi" } ); + player2.write(0x01, { message: "hi" } ); }); var player2 = mc.createClient({ username: 'player2' }); }); @@ -558,7 +596,7 @@ describe("mc-server", function() { if (!server.clients.hasOwnProperty(clientId)) continue; client = server.clients[clientId]; - if (client !== exclude) client.write(0x03, { message: message }); + if (client !== exclude) client.write(0x02, { message: JSON.stringify({text: message})}); } } }); @@ -610,7 +648,7 @@ describe("mc-server", function() { }); server.on('listening', function() { var client = mc.createClient({ username: 'lalalal', }); - client.on(0x01, function() { + client.on([states.PLAY, 0x01], function() { server.close(); }); });