Created
August 1, 2011 22:21
-
-
Save TobiX/1119136 to your computer and use it in GitHub Desktop.
Minecraft "protocol parser"
This file contains hidden or 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