Created
August 1, 2011 22:21
-
-
Save TobiX/1119136 to your computer and use it in GitHub Desktop.
Minecraft "protocol parser"
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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