Skip to content

Instantly share code, notes, and snippets.

@TobiX
Created August 1, 2011 22:21
Show Gist options
  • Save TobiX/1119136 to your computer and use it in GitHub Desktop.
Save TobiX/1119136 to your computer and use it in GitHub Desktop.
Minecraft "protocol parser"
package MineCrap::Protocol;
use common::sense;
use Moose;
use Moose::Util::TypeConstraints;
#use namespace::autoclean;
use Data::ParseBinary;
use MineCrap::Protocol::CompressionAdapter;
use MineCrap::Protocol::DoubleAdapter;
use MineCrap::Protocol::UCS2Adapter;
our $VERSION = '0.010';
our $PROTOCOL_VERSION = 11;
# define if this filter handles communication with a game server or a client
enum 'ProtocolMode' => qw(server client);
has 'mode', is => 'ro', isa => 'ProtocolMode', default => 'server';
has 'data', is => 'ro', isa => 'Str', default => '', writer => '_set_data';
sub _compression {
return MineCrap::Protocol::CompressionAdapter->create($_[0]);
}
sub _utf8string {
# We can't use the encoding parameter here, because Data::ParseBinary then
# assumes the length is the number of characters, but in the MC protocol it
# is the number of bytes!
return PascalString($_[0], \&UBInt16);
}
sub _double {
return MineCrap::Protocol::DoubleAdapter->create(UBInt16($_[0]));
}
sub _ucs2string {
return MineCrap::Protocol::UCS2Adapter->create(PascalString($_[0], \&_double));
}
sub _build_parser
{
my $mode = shift;
my $face = Enum(UBInt8("face"),
'-Y' => 0,
'+Y' => 1,
'-Z' => 2,
'+Z' => 3,
'-X' => 4,
'+X' => 5,
_default_ => $DefaultPass,
);
my $object_type = Enum(UBInt8("type"),
boat => 1,
minecart => 10,
storage_cart => 11,
powered_cart => 12,
tnt => 50,
arrow => 60,
snowball => 61,
egg => 62,
sand => 70,
gravel => 71,
fishing_rod => 90,
_default_ => $DefaultPass,
);
my $ani_type = Enum(UBInt8("animate"),
no_ani => 0,
swing_arm => 1,
damage => 2,
crouch => 104,
uncrouch => 105,
unknown102 => 102,
_default_ => $DefaultPass,
);
my $mob_type = Enum(UBInt8("type"),
creeper => 50,
skeleton => 51,
spider => 52,
giant_zombie => 53,
zombie => 54,
slime => 55,
ghast => 56,
zombie_pigman => 57,
pig => 90,
sheep => 91,
cow => 92,
hen => 93,
squid => 94,
_default_ => $DefaultPass,
);
# Not fully understood...
my $ent_status = Enum(UBInt8("status"),
hurt => 2,
dead => 3, # maybe?
unk4 => 4,
unk5 => 5,
_default_ => $DefaultPass,
);
my $instrument = Enum(UBInt8("instrument"),
harp => 0,
double_bass => 1,
snare_drum => 2,
clicks => 3,
bass_drum => 4,
_default_ => $DefaultPass,
);
my $inv_type = Enum(UBInt8("type"),
chest => 0,
workbench => 1,
furnace => 2,
dispenser => 3,
_default_ => $DefaultPass,
);
my $state_type = Enum(UBInt8("type"),
bed_invalid => 0,
rain_start => 1,
rain_end => 2,
_default_ => $DefaultPass,
);
# FIXME!
my $metadata = RepeatUntil(sub { $_->obj == 0x7F }, UBInt8("data"));
Struct("packet",
Enum(UBInt8("type"),
KEEP_ALIVE => 0x00,
LOGIN => 0x01,
HANDSHAKE => 0x02,
CHAT_MSG => 0x03,
TIME => 0x04,
ENT_EQUIPMENT => 0x05,
SPAWN_POS => 0x06,
USE_ENT => 0x07, # unsure?
HEALTH => 0x08,
RESPAWN => 0x09,
P_GROUND => 0x0A, # called "player" or "flying" on wiki.vg
P_POSITION => 0x0B,
P_LOOK => 0x0C,
P_POSITON_LOOK => 0x0D,
DIGGING => 0x0E,
PLACE_BLOCK => 0x0F,
CHANGE_HOLDING => 0x10,
USE_BED => 0x11,
ANIMATION => 0x12,
ENT_ACTION => 0x13, # unsure? crouching?
ENT_SPAWN => 0x14, # only players, NPCs, not mobs
PICKUP_SPAWN => 0x15,
COLLECT_ITEM => 0x16,
ADD_OBJECT => 0x17, # object/vehicle
MOB_SPAWN => 0x18,
ENT_PAINTING => 0x19,
UNKNOWN_1B => 0x1B, # not decoded yet
ENT_VELOCITY => 0x1C, # unsure?
ENT_DESTROY => 0x1D,
ENTITY => 0x1E, # entity is idle
ENT_REL_MOVE => 0x1F,
ENT_LOOK => 0x20,
ENT_LOOK_REL => 0x21,
ENT_TELEPORT => 0x22,
ENT_STATUS => 0x26, # unsure?
ATTACH_ENT => 0x27, # unsure?
ENT_METADATA => 0x28,
PRE_CHUNK => 0x32,
MAP_CHUNK => 0x33,
M_BLOCK_CHANGE => 0x34,
BLOCK_CHANGE => 0x35,
PLAY_NOTE => 0x36,
EXPLOSION => 0x3C,
STATE => 0x46,
WEATHER => 0x47, # unsure?
WINDOW_OPEN => 0x64,
WINDOW_CLOSE => 0x65,
WINDOW_CLICK => 0x66, # unsure?
SET_SLOT => 0x67, # unsure?
WINDOW_ITEMS => 0x68,
PROGRESS_BAR => 0x69,
TRANSACTION => 0x6A,
UPDATE_SIGN => 0x82,
STAT_INCREMENT => 0xC8,
DISCONNECT => 0xFF,
),
Switch("payload", sub { $_->ctx->{type} },
{
KEEP_ALIVE => $DefaultPass,
LOGIN => Switch("mode", sub { $mode },
{
'client' => Struct("client", Const(SBInt32("version"), $PROTOCOL_VERSION),
_ucs2string("username"), SBInt64("seed"), SBInt8("dimension")),
'server' => Struct("server", SBInt32("ent_id"),
_ucs2string("unknown1"), SBInt64("seed"), SBInt8("dimension")),
}),
HANDSHAKE => Switch("mode", sub { $mode },
{
'client' => Struct("client", _ucs2string("username")),
'server' => Struct("server", _ucs2string("conn_hash")),
}),
CHAT_MSG => Struct("chat", _ucs2string("message")),
TIME => Struct("time", SBInt64("time")),
ENT_EQUIPMENT => Struct("equip", SBInt32("ent_id"), SBInt16("slot"), SBInt16("item_id"), SBInt16("unknown1")),
SPAWN_POS => Struct("spawn", SBInt32("x"), SBInt32("y"), SBInt32("z")),
USE_ENT => Struct("ent", SBInt32("user"), SBInt32("target"), Flag("leftclick")),
HEALTH => Struct("health", SBInt16("health")),
RESPAWN => $DefaultPass,
P_GROUND => Struct("ground", Flag("ground")),
P_POSITION => Struct("pos", BFloat64("x"), BFloat64("y"), BFloat64("stance"), BFloat64("z"), Flag("ground")),
P_LOOK => Struct("look", BFloat32("yaw"), BFloat32("pitch"), Flag("ground")),
P_POSITON_LOOK => Switch("mode", sub { $mode },
{
'client' => Struct("client", BFloat64("x"), BFloat64("stance"), BFloat64("y"),
BFloat64("z"), BFloat32("yaw"), BFloat32("pitch"), Flag("ground")),
'server' => Struct("server", BFloat64("x"), BFloat64("y"), BFloat64("stance"),
BFloat64("z"), BFloat32("yaw"), BFloat32("pitch"), Flag("ground")),
}),
DIGGING => Struct("digging",
Enum(UBInt8("status"),
start => 0,
finish => 2,
drop => 4,
_default_ => $DefaultPass,
), SBInt32("x"), SBInt8("y"), SBInt32("z"), $face),
PLACE_BLOCK => Struct("place", SBInt32("x"), SBInt8("y"), SBInt32("z"), $face, SBInt16("item_id"),
If(sub { $_->ctx->{item_id} != -1 }, Struct("itemdata", SBInt8("count"), SBInt16("damage")))),
CHANGE_HOLDING => Struct("change", SBInt16("slot_id")),
USE_BED => Struct("use_bed", SBInt32("ent_id"), SBInt8("in_bed"), SBInt32("x"), SBInt8("y"), SBInt32("z")),
ANIMATION => Struct("ani", SBInt32("ent_id"), $ani_type),
ENT_ACTION => Struct("ent_action", SBInt32("ent_id"),
Enum(UBInt8("action"),
crouch => 1,
uncrouch => 2,
_default_ => $DefaultPass,
)),
ENT_SPAWN => Struct("espawn", SBInt32("ent_id"), _ucs2string("name"),
SBInt32("x"), SBInt32("y"), SBInt32("z"), SBInt8("rotation"), SBInt8("pitch"), SBInt16("cur_item")),
PICKUP_SPAWN => Struct("pspawn", SBInt32("ent_id"), SBInt16("item_id"), SBInt8("count"), SBInt16("damage"),
SBInt32("x"), SBInt32("y"), SBInt32("z"), SBInt8("rotation"), SBInt8("pitch"), SBInt8("roll")),
COLLECT_ITEM => Struct("collect", SBInt32("collected"), SBInt32("collector")),
ADD_OBJECT => Struct("object", SBInt32("ent_id"), $object_type, SBInt32("x"), SBInt32("y"), SBInt32("z")),
MOB_SPAWN => Struct("mspawn", SBInt32("ent_id"), $mob_type, SBInt32("x"), SBInt32("y"), SBInt32("z"),
SBInt8("yaw"), SBInt8("pitch"), $metadata),
ENT_PAINTING => Struct("painting", SBInt32("ent_id"), _ucs2string("name"),
SBInt32("x"), SBInt32("y"), SBInt32("z"), SBInt32("type")),
UNKNOWN_1B => Struct("unk1B", BFloat32("unk1"), BFloat32("unk2"), BFloat32("unk3"), BFloat32("unk4"),
Flag("unk5"), Flag("unk6")),
ENT_VELOCITY => Struct("velocity", SBInt32("ent_id"), SBInt16("x"), SBInt16("y"), SBInt16("z")),
ENT_DESTROY => Struct("destroy", SBInt32("ent_id")),
ENTITY => Struct("ent", SBInt32("ent_id")),
ENT_REL_MOVE => Struct("ermove", SBInt32("ent_id"), SBInt8("dx"), SBInt8("dy"), SBInt8("dz")),
ENT_LOOK => Struct("elook", SBInt32("ent_id"), SBInt8("yaw"), SBInt8("pitch")),
ENT_LOOK_REL => Struct("lookmove", SBInt32("ent_id"),
SBInt8("dx"), SBInt8("dy"), SBInt8("dz"), SBInt8("yaw"), SBInt8("pitch")),
ENT_TELEPORT => Struct("teleport", SBInt32("ent_id"),
SBInt32("x"), SBInt32("y"), SBInt32("z"), SBInt8("yaw"), SBInt8("pitch")),
ENT_STATUS => Struct("estatus", SBInt32("ent_id"), $ent_status),
ENT_ATTACH => Struct("eattach", SBInt32("ent_id"), SBInt32("vehicle_id")),
ENT_METADATA => Struct("emeta", SBInt32("ent_id"), $metadata),
PRE_CHUNK => Struct("prechunk", SBInt32("x"), SBInt32("z"), Flag("mode")),
MAP_CHUNK => Struct("mapchunk", SBInt32("x"), SBInt16("y"), SBInt32("z"),
SBInt8("size_x"), SBInt8("size_y"), SBInt8("size_z"),
_compression(PascalString("region", \&UBInt32))),
M_BLOCK_CHANGE => Struct("mblockchange", SBInt32("x"), SBInt32("z"), SBInt16("size"),
Array(sub { $_->ctx->{size} }, BitStruct("coord", Nibble("x"), Nibble("z"), Octet("y"))),
Array(sub { $_->ctx->{size} }, SBInt8("type")),
Array(sub { $_->ctx->{size} }, SBInt8("metadata"))),
BLOCK_CHANGE => Struct("blockchange", SBInt32("x"), SBInt8("y"), SBInt32("z"), SBInt8("type"), SBInt8("metadata")),
PLAY_NOTE => Struct("note", SBInt32("x"), SBInt16("y"), SBInt32("z"), $instrument, SBInt8("pitch")),
EXPLOSION => Struct("explosion", BFloat64("x"), BFloat64("y"), BFloat64("z"), BFloat32("unk1"), SBInt32("count"),
Array(sub { $_->ctx->count }, Struct("records", SBInt8("x"), SBInt8("y"), SBInt8("z")))),
STATE => Struct("state", $state_type),
WEATHER => Struct("weather", SBInt32("ent_id"), Flag("raining"), SBInt32("x"), SBInt32("y"), SBInt32("z")),
WINDOW_OPEN => Struct("win", SBInt8("win_id"), $inv_type, _utf8string("title"), SBInt8("slots")),
WINDOW_CLOSE => Struct("close", SBInt8("win_id")),
WINDOW_CLICK => Struct("click", SBInt8("win_id"), SBInt16("slot"), Flag("rightclick"),
SBInt16("action"), Flag("shift"), SBInt16("item_id"),
If(sub { $_->ctx->{item_id} != -1 }, Struct("itemdata", SBInt8("count"), SBInt16("damage")))),
SET_SLOT => Struct("slot", SBInt8("win_id"), SBInt16("slot"), SBInt16("item_id"),
If(sub { $_->ctx->{item_id} != -1 }, Struct("itemdata", SBInt8("count"), SBInt16("damage")))),
WINDOW_ITEMS => Struct("items", SBInt8("win_id"), SBInt16("count"),
Array(sub { $_->ctx->{count} }, Struct("item", SBInt16("item_id"),
If(sub { $_->ctx->{item_id} != -1 }, Struct("itemdata", SBInt8("count"), SBInt16("damage")))))),
PROGRESS_BAR => Struct("progress", SBInt8("win_id"), SBInt16("bar"), SBInt16("value")),
TRANSACTION => Struct("trans", SBInt8("win_id"), SBInt16("action"), Flag("accepted")),
UPDATE_SIGN => Struct("sign", SBInt32("x"), SBInt16("y"), SBInt32("z"), Array(4, _ucs2string("title"))),
STAT_INCREMENT => Struct("statistics", SBInt32("stat_id"), SBInt8("amount")),
DISCONNECT => Struct("disconnect", _ucs2string("reason")),
})
);
};
my %parsers = (
client => _build_parser('client'),
server => _build_parser('server'),
);
sub get_one_start
{
my ($self, $data) = @_;
$self->_set_data($self->data . join '', @$data);
}
sub get_one
{
my $self = shift;
return [] unless length $self->data;
my $stream = CreateStreamReader(StringRef => \$self->data);
my $data = eval { $parsers{$self->mode}->parse($stream); };
if ($@ ne '') {
die unless $@ =~ /not enought bytes in stream/;
return [];
}
$self->_set_data(substr($self->data, $stream->tell));
return [ $data ];
}
sub get
{
my ($self, $stream) = @_;
my @return;
$self->get_one_start($stream);
while (1) {
my $next = $self->get_one();
last unless @$next;
push @return, @$next;
}
return \@return;
}
sub put
{
my ($self, $packets) = @_;
my @out;
foreach my $packet (@$packets) {
push @out, $parsers{($self->mode eq 'client')?'server':'client'}->build($packet);
}
\@out;
}
sub get_pending
{
my $self = shift;
return [ $self->data ] if length $self->data;
return undef;
}
__PACKAGE__->meta->make_immutable;
1;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment