diff --git a/index.js b/index.js index 1d2ecbf..6fd0f11 100644 --- a/index.js +++ b/index.js @@ -24,6 +24,7 @@ function createServer(options) { options['server-port'] != null ? options['server-port'] : 25565 ; + var maxPlayers = options['max-players'] || 20; var host = options.host || '0.0.0.0'; var kickTimeout = options.kickTimeout || 10 * 1000; var checkTimeoutInterval = options.checkTimeoutInterval || 4 * 1000; @@ -32,6 +33,7 @@ function createServer(options) { assert.ok(! onlineMode, "online mode for servers is not yet supported"); var server = new Server(options); + server.maxPlayers = maxPlayers; server.on("connection", function(client) { client.once(0xfe, onPing); client.on(0x02, onHandshake); @@ -87,7 +89,7 @@ function createServer(options) { protocol.minecraftVersion, motd, server.playerCount, - server.maxPlayers, + maxPlayers, ].join('\u0000') }); } @@ -118,9 +120,7 @@ function createClient(options) { var email = options.email; var password = options.password; - var client = new Client({ - isServer: false - }); + var client = new Client(false); client.username = options.username; client.on('connect', function() { client.write(0x02, { diff --git a/lib/client.js b/lib/client.js index cabe8a7..3166a6c 100644 --- a/lib/client.js +++ b/lib/client.js @@ -7,12 +7,10 @@ var net = require('net') module.exports = Client; -function Client(options) { +function Client(isServer) { EventEmitter.call(this); - options = options || {}; - - this.isServer = !!options.isServer; + this.isServer = !!isServer; this.socket = null; this.encryptionEnabled = false; this.cipher = null; diff --git a/lib/protocol.js b/lib/protocol.js index 9d35ded..132b9f7 100644 --- a/lib/protocol.js +++ b/lib/protocol.js @@ -2,15 +2,6 @@ var assert = require('assert'); var STRING_MAX_LENGTH = 240; -module.exports = { - version: 51, - minecraftVersion: '1.4.7', - sessionVersion: 13, - parsePacket: parsePacket, - createPacketBuffer: createPacketBuffer, - STRING_MAX_LENGTH: STRING_MAX_LENGTH, -}; - var packets = { 0x00: [ { name: "keepAliveId", type: "int" } @@ -459,10 +450,10 @@ var types = { 'double': [readDouble, DoubleWriter], 'float': [readFloat, FloatWriter], 'slot': [readSlot, SlotWriter], + 'long': [readLong, LongWriter], + 'ascii': [readAscii, AsciiWriter], - 'ascii': [readAscii], 'byteArray32': [readByteArray32], - 'long': [readLong], 'slotArray': [readSlotArray], 'mapChunkBulk': [readMapChunkBulk], 'entityMetadata': [readEntityMetadata], @@ -886,6 +877,22 @@ StringWriter.prototype.write = function(buffer, offset) { } }; +function AsciiWriter(value) { + this.value = value; + this.size = 2 + value.length; +} + +AsciiWriter.prototype.write = function(buffer, offset) { + var cursor = offset; + buffer.writeInt16BE(this.value.length, cursor); + cursor += 2; + + for (var i = 0; i < this.value.length; ++i) { + buffer.writeUInt8(this.value.charCodeAt(i), cursor); + cursor += 1; + } +}; + function ByteArray16Writer(value) { assert.ok(Buffer.isBuffer(value), "non buffer passed to ByteArray16Writer"); this.value = value; @@ -969,6 +976,16 @@ IntWriter.prototype.write = function(buffer, offset) { buffer.writeInt32BE(this.value, offset); } +function LongWriter(value) { + this.value = value; + this.size = 8; +} + +LongWriter.prototype.write = function(buffer, offset) { + buffer.writeInt32BE(this.value[0], offset); + buffer.writeInt32BE(this.value[1], offset + 4); +} + function get(packetId, toServer) { var packetInfo = packets[packetId]; return Array.isArray(packetInfo) ? @@ -1025,3 +1042,14 @@ function parsePacket(buffer, isServer) { results: results, }; } + +module.exports = { + version: 51, + minecraftVersion: '1.4.7', + sessionVersion: 13, + parsePacket: parsePacket, + createPacketBuffer: createPacketBuffer, + STRING_MAX_LENGTH: STRING_MAX_LENGTH, + packets: packets, + get: get, +}; diff --git a/lib/server.js b/lib/server.js index b78951f..fd40db2 100644 --- a/lib/server.js +++ b/lib/server.js @@ -6,10 +6,9 @@ var net = require('net') module.exports = Server; -function Server(options) { +function Server() { EventEmitter.call(this); - this.maxPlayers = options['max-players'] || 20; this.playerCount = 0 this.socketServer = null; @@ -24,9 +23,7 @@ Server.prototype.listen = function(port, host) { var nextId = 0; self.socketServer = net.createServer(); self.socketServer.on('connection', function(socket) { - var client = new Client({ - isServer: true, - }); + var client = new Client(true); client.id = nextId++; self.clients[client.id] = client; client.on('error', function(err) { diff --git a/package.json b/package.json index 292921e..c1bf6b7 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "devDependencies": { "mocha": "~1.7.4", "mkdirp": "~0.3.4", - "rimraf": "~2.1.1" + "rimraf": "~2.1.1", + "zfill": "0.0.1" }, "dependencies": { "ursa": "~0.8.0", diff --git a/test/test.js b/test/test.js index cdd6d3a..0ca76dc 100644 --- a/test/test.js +++ b/test/test.js @@ -1,12 +1,16 @@ var mc = require('../') - , protocol = require('../lib/protocol') + , protocol = mc.protocol + , Client = mc.Client + , Server = mc.Server , spawn = require('child_process').spawn , path = require('path') , fs = require('fs') + , net = require('net') , assert = require('assert') , mkdirp = require('mkdirp') , rimraf = require('rimraf') , Batch = require('batch') + , zfill = require('zfill') , MC_SERVER_JAR = process.env.MC_SERVER_JAR , SURVIVE_TIME = 10000 , MC_SERVER_PATH = path.join(__dirname, 'server') @@ -41,6 +45,125 @@ var defaultServerProps = { 'motd': 'A Minecraft Server', }; +var values = { + 'int': Math.floor(Math.random() * Math.pow(2, 16)), + 'short': Math.floor(Math.random() * Math.pow(2, 8)), + 'ushort': Math.floor(Math.random() * Math.pow(2, 16)), + 'byte': Math.floor(Math.random() * Math.pow(2, 4)), + 'ubyte': Math.floor(Math.random() * Math.pow(2, 8)), + 'string': "hi hi this is my string", + 'byteArray16': new Buffer(8), + 'bool': Math.random() < 0.5, + 'double': Math.random() * Math.pow(2, 64), + 'float': Math.random() * Math.pow(2, 32), + 'slot': { + id: 5, + itemCount: 56, + itemDamage: 2, + nbtData: new Buffer(90), + }, + + 'ascii': "hello", + 'byteArray32': new Buffer(10), + 'long': [0, 1], + 'slotArray': [{ + id: 41, + itemCount: 2, + itemDamage: 3, + nbtData: new Buffer(0), + }], + 'mapChunkBulk': { + skyLightSent: true, + compressedChunkData: new Buffer(1234), + meta: [{ + x: 23, + z: 64, + bitMap: 3, + addBitMap: 10, + }], + }, + 'entityMetadata': {}, + 'objectData': { + intField: 9, + velocityX: 1, + velocityY: 2, + velocityZ: 3, + }, + '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}], +} + +describe("packets", function() { + var client, server, serverClient; + before(function(done) { + server = new Server(); + server.once('listening', function() { + server.once('connection', function(c) { + serverClient = c; + done(); + }); + client = new Client(); + client.setSocket(net.connect(25565, 'localhost')); + }); + server.listen(25565, 'localhost'); + }); + after(function(done) { + client.on('end', function() { + server.on('close', done); + server.close(); + }); + client.end(); + }); + var packetId, packetInfo, field; + for(packetId in protocol.packets) { + packetId = parseInt(packetId, 10); + packetInfo = protocol.packets[packetId]; + it("0x" + zfill(parseInt(packetId, 10).toString(16), 2), callTestPacket(packetId, packetInfo)); + } + function callTestPacket(packetId, packetInfo) { + 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(done); + }; + } + function testPacket(packetId, packetInfo, done) { + // empty object uses default values + var packet = {}; + packetInfo.forEach(function(field) { + var value = field.type; + 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); + done(); + }); + serverClient.write(packetId, receivedPacket); + }); + client.write(packetId, packet); + } + function assertPacketsMatch(p1, p2) { + var field; + for (field in p1) { + assert.ok(field in p2, "field " + field + " missing in p2") + } + for (field in p2) { + assert.ok(field in p1, "field " + field + " missing in p1"); + } + } +}); + describe("client", function() { this.timeout(20000);