Skip to content

Instantly share code, notes, and snippets.

@mappum
Created February 14, 2012 08:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mappum/1824868 to your computer and use it in GitHub Desktop.
Save mappum/1824868 to your computer and use it in GitHub Desktop.
(function(Minecraft) {
/** @constructor */
Minecraft.Client = function(args) {
if(Minecraft.ENVIRONMENT == 'node') {
if(typeof Minecraft.node.net == 'undefined') Minecraft.node.net = require('net');
if(typeof Minecraft.node.http == 'undefined') Minecraft.node.http = require('http');
if(typeof Minecraft.node.https == 'undefined') Minecraft.node.https = require('https');
if(typeof Minecraft.node.querystring == 'undefined') Minecraft.node.querystring = require('querystring');
this.host = args.host;
this.port = args.port || 25565;
this.username = args.username || 'Player';
this.password = args.password;
this.protocol = args.protocol || 23;
this.version = args.version || 12;
this.currentVersion;
this.downloadTicket;
this.sessionId;
this._status = 0;
this._active = false;
this._error = '';
this.socket = new Minecraft.node.net.Socket();
this.socket.on('data', function(data) {
var buf = new Buffer(data);
if(typeof Minecraft.protocol[buf[0]] != 'undefined') {
var packet = Minecraft.protocol[buf[0]].decode(data);
var packetData = packet.toObject();
console.log(packet);
switch(buf[0]) {
case 0x00:
if(this._active) {
this.send(Minecraft.protocol[0x00].encode(packetData));
}
break;
case 0x01:
if(this._status == 2) this._status = 3;
case 0x02:
if(this._status == 1) {
Minecraft.node.http.get({
host: 'session.minecraft.net',
path: '/game/joinserver.jsp?' +
Minecraft.node.querystring.stringify({
user: this.username,
sessionId: this.sessionId,
serverId: packetData.connectionHash
})
}, function(res) {
res.on('data', function(chunk) {
console.log(chunk.toString());
if(chunk.toString() == 'OK') {
this._status = 2;
this.send(Minecraft.protocol[0x01].encode({
protocolVersion: this.protocol,
username: this.username
}));
} else {
this.error(chunk.toString());
}
}.bind(this));
}.bind(this));
}
break;
}
} else {
console.log('Got unsupported packet (0x' + buf[0].toString(16) + ')');
}
}.bind(this));
this.socket.on('end', function() {
this.error('Connection closed');
});
} else {
throw new Error('Only supported in node');
}
};
Minecraft.Client.prototype.login = function(callback) {
this._active = true;
var postData = Minecraft.node.querystring.stringify({
user: this.username,
password: this.password,
version: this.version
});
var req = Minecraft.node.https.request({
host: 'login.minecraft.net',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': postData.length
}
}, function(res) {
res.on('data', function (chunk) {
if(this._status == 0) {
console.log(chunk.toString());
var split = chunk.toString().split(':');
if(split.length == 4) {
this.currentVersion = split[0];
this.downloadTicket = split[1];
this.username = split[2];
this.sessionId = split[3];
this._status = 1;
this.socket.connect(this.port, this.host, function() {
this.send(Minecraft.protocol[0x02].encode({
username: this.username
}));
}.bind(this));
var keepAlive = function() {
Minecraft.node.https.get({
host: 'login.minecraft.net',
path: '/session?' +
Minecraft.node.querystring.stringify({
name: this.username,
session: this.sessionId
})
});
setInterval(keepAlive, 6000 * Minecraft.MS_PER_TICK);
}.bind(this);
keepAlive();
} else {
this.error(chunk.toString());
}
}
}.bind(this));
}.bind(this));
req.write(postData);
};
Minecraft.Client.prototype.send = function(packet) {
if(this._active) {
this.socket.write(packet.toString());
console.log('>>');
console.log(packet.toString('hex'));
}
};
Minecraft.Client.prototype.error = function(message) {
console.log('ERROR: ' + message + ' (' + this._status + ')');
this._status = -1;
this._error = message;
this._active = false;
this.socket.destroy();
};
})(Minecraft);
(function(Minecraft) {
/** @constructor */
Minecraft.Packet = function(id, data) {
if(Minecraft.ENVIRONMENT == 'node') {
this.id = id;
this.data = data || [];
} else {
throw new Error('Only supported in node');
}
};
Minecraft.Packet.prototype.get = function(key) {
if(typeof key == 'number') return this.data[key].value;
else if(typeof key == 'string') {
for(var i = 0; i < this.data.length; i++) {
if(this.data[i].name == key) return this.data[i].value;
}
}
};
Minecraft.Packet.prototype.append = function(name, type, value) {
this.data.push({name: name, type: type, value: value});
};
Minecraft.Packet.prototype.toString = function(encoding) {
var buf = new Buffer(this.getSize());
var offset = 0;
buf.writeInt8(this.id, offset);
offset++;
for(var i = 0; i < this.data.length; i++) {
switch(this.data[i].type.toLowerCase()) {
case 'byte':
buf.writeInt8(this.data[i].value, offset);
break;
case 'ubyte':
buf.writeUInt8(this.data[i].value, offset);
break;
case 'short':
buf.writeInt16BE(this.data[i].value, offset);
break;
case 'int':
buf.writeInt32BE(this.data[i].value, offset);
break;
case 'long':
buf.writeInt32BE(this.data[i].value >> 4, offset);
buf.writeInt32BE(this.data[i].value << 4 >> 4, offset + 4);
break;
case 'float':
buf.writeFloatBE(this.data[i].value, offset);
break;
case 'double':
buf.writeDoubleBE(this.data[i].value, offset);
break;
case 'string':
buf.writeInt16BE(this.data[i].value.length, offset);
offset += 2;
if(this.data[i].value) {
buf.write(this.data[i].value, offset, this.data[i].value.length * 2, 'ucs2');
buf.switchEndian(offset, this.data[i].value.length * 2);
}
break;
case 'bool':
buf.writeInt8(this.data[i].value, offset);
break;
}
if(this.data[i].type.toLowerCase() != 'string') {
offset += Minecraft.Packet.prototype.types[this.data[i].type.toUpperCase()].size;
} else {
offset += this.data[i].value.length * 2;
}
}
return buf.toString(encoding || 'utf8');
};
Minecraft.Packet.prototype.toObject = function() {
var output = {};
for(var i = 0; i < this.data.length; i++) {
output[this.data[i].name] = this.data[i].value;
}
return output;
};
Minecraft.Packet.prototype.getSize = function() {
var output = 1;
for(var i = 0; i < this.data.length; i++) {
if(this.data[i].type.toLowerCase() != 'string') {
output += Minecraft.Packet.prototype.types[this.data[i].type.toUpperCase()].size;
} else {
if(this.data[i].value) output += this.data[i].value.length * 2;
output += 2;
}
}
return output;
};
Minecraft.Packet.prototype.types = {
BYTE: {size: 1},
UBYTE: {size: 1},
SHORT: {size: 2},
INT: {size: 4},
LONG: {size: 8},
FLOAT: {size: 4},
DOUBLE: {size: 8},
STRING: {size: 2},
BOOL: {size: 1}
};
})(Minecraft);
(function(Minecraft) {
/** @constructor */
Minecraft.PacketFormat = function(id, toServer, toClient) {
if(Minecraft.ENVIRONMENT == 'node') {
this.id = id;
if(typeof toServer != 'undefined') {
if(!Array.isArray(toServer)) {
this.toServer = [toServer];
} else {
this.toServer = toServer;
}
} else {
throw new Error('Format was not specified');
}
if(typeof toClient != 'undefined') {
if(!Array.isArray(toClient)) {
this.toClient = [toClient];
} else {
this.toClient = toClient;
}
} else {
this.toClient = this.toServer;
}
} else {
throw new Error('Only supported in node');
}
};
Minecraft.PacketFormat.prototype.encode = function(data, toClient) {
var packet = new Minecraft.Packet(this.id);
var format = this.toServer;
if(toClient) format = this.toClient;
for(var i = 0; i < format.length; i++) {
if(typeof data[format[i].name] != 'undefined') {
packet.append(format[i].name, format[i].type, data[format[i].name]);
} else {
var value;
switch(format[i].type) {
case 'string':
value = '';
break;
case 'bool':
value = false;
break;
default:
value = 0;
break;
}
packet.append(format[i].name, format[i].type, value);
}
}
return packet;
};
Minecraft.PacketFormat.prototype.decode = function(data, toServer) {
var format = this.toClient;
if(toServer) format = this.toServer;
var buf = new Buffer(data);
var packet = new Minecraft.Packet(buf[0]);
var offset = 1;
for(var i = 0; i < format.length; i++) {
var value, stringLength = 0;
switch(format[i].type.toLowerCase()) {
case 'byte':
value = buf.readInt8(offset);
break;
case 'ubyte':
value = buf.readUInt8(offset);
break;
case 'short':
value = buf.readInt16BE(offset);
break;
case 'int':
value = buf.readInt32BE(offset);
break;
case 'long':
//TODO: figure out how to handle 64-bit
value = buf.readInt32BE(offset);
break;
case 'float':
value = buf.readFloatBE(offset);
break;
case 'double':
value = buf.readDoubleBE(offset);
break;
case 'string':
stringLength = buf.readInt16BE(offset);
offset += 2;
buf.switchEndian(offset, stringLength * 2);
value = '';
for(var j = 0; j < stringLength * 2; j+=2) {
value += String.fromCharCode(buf.readInt8(offset + j) + buf.readInt8(offset + j + 1));
}
break;
case 'bool':
value = buf.readInt8(offset);
break;
}
packet.append(format[i].name, format[i].type, value);
if(format[i].type.toLowerCase() != 'string') {
offset += Minecraft.Packet.prototype.types[format[i].type.toUpperCase()].size;
} else {
offset += stringLength * 2;
}
}
return packet;
};
})(Minecraft);
(function(Minecraft) {
Minecraft.protocol = {
0x00:
new Minecraft.PacketFormat(0x00,
{type: 'int', name: 'keepAliveId'}
),
0x01:
new Minecraft.PacketFormat(0x01, [
{type: 'int', name: 'protocolVersion'},
{type: 'string', name: 'username'},
{type: 'long', name: 'mapSeed'},
{type: 'string', name: 'levelType'},
{type: 'int', name: 'serverMode'},
{type: 'byte', name: 'dimension'},
{type: 'byte', name: 'difficulty'},
{type: 'ubyte', name: 'worldHeight'},
{type: 'ubyte', name: 'maxPlayers'}
]),
0x02:
new Minecraft.PacketFormat(0x02,
//to server
{type: 'string', name: 'username'},
//to client
{type: 'string', name: 'connectionHash'}
)
};
})(Minecraft);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment