Skip to content
Browse files

Initial commit: a really basic MineCraft bot that can respond to chat…

… commands.
  • Loading branch information...
0 parents commit 9dd2749b10009d9fbc02f581f092375cc8617e36 @sneakin committed Oct 5, 2011
Showing with 1,974 additions and 0 deletions.
  1. +2 −0 .gitignore
  2. +163 −0 bin/mc_bot
  3. +26 −0 bin/wiretap.rb
  4. +192 −0 client_session.log
  5. +134 −0 client_session_dig.log
  6. +16 −0 lib/mc.rb
  7. +206 −0 lib/mc/client.rb
  8. +36 −0 lib/mc/connection.rb
  9. +703 −0 lib/mc/packet.rb
  10. +87 −0 lib/mc/parser.rb
  11. +212 −0 lib/mc/request.rb
  12. +29 −0 lib/mc/session.rb
  13. +15 −0 lib/mc/string16.rb
  14. +37 −0 lib/mc/type_id_factory.rb
  15. 0 lib/mc_bot.rb
  16. +116 −0 test/mc_bot_test.rb
2 .gitignore
@@ -0,0 +1,2 @@
+*~
+log
163 bin/mc_bot
@@ -0,0 +1,163 @@
+#!/bin/env ruby
+
+require 'mc'
+require 'mc/request'
+require 'open-uri'
+
+class Bot < MC::Client
+ def on_keep_alive(packet)
+ puts "KEEP ALIVE: #{packet.keep_alive_id}"
+ super
+ end
+
+ def entity_count
+ entities.
+ collect { |eid, data| data }.
+ group_by { |e| e.mob_type }.
+ collect { |type, e| [type, e.count ] }
+ end
+
+ def named_entities
+ entities.
+ inject([]) { |acc, (eid, data)| acc << data if data.kind_of?(MC::Client::NamedEntity); acc }
+ end
+
+ def crouch
+ change_stance_to(0.8)
+ send_packet(MC::EntityAction.new(0, MC::EntityAction::Crouch))
+ end
+
+ def stand
+ change_stance_to(1.5)
+ send_packet(MC::EntityAction.new(0, MC::EntityAction::Stand))
+ end
+
+ def on_chat_message(packet)
+ super
+
+ m = packet.message.match(/<(\w+)> (.*)/)
+ if m
+ return if m[1] != 'SneakyDean'
+ body = m[2]
+ else
+ m = packet.message.match(/[Server] (.*)/)
+ return if m == nil
+ body = m[1]
+ end
+
+ case body
+ when /hello/i then send_packet(MC::ChatMessage.new("Hello"))
+ when /say (.*)/ then send_packet(MC::ChatMessage.new($1))
+ #when /look (-?\d+) (-?\d+)/ then send_packet(MC::PlayerPositionAndLook.new(x_absolute, y_absolute, z_absolute, stance, $1.to_i, $2.to_i, on_ground)) && send_packet(MC::ChatMessage.new("looking at #{$1} #{$2}"))
+ when /look (-?\d+) (-?\d+)/ then send_packet(MC::PlayerLook.new($1.to_i, $2.to_i, on_ground)) && send_packet(MC::ChatMessage.new("looking at #{$1} #{$2}"))
+ #when /move (-?\d+) (-?\d+)/ then send_packet(MC::PlayerPositionAndLook.new(x_absolute + $1.to_i, y_absolute, z_absolute + $2.to_i, stance, yaw, pitch, on_ground)) && send_packet(MC::ChatMessage.new("moving to #{x_absolute + $1.to_i} #{z_absolute + $2.to_i}"))
+ when /move (-?\d+) (-?\d+) (-?\d+)/ then move_by($1.to_i, $2.to_i, $3.to_i) && send_packet(MC::ChatMessage.new("moving to #{x + $1.to_i} #{y + $2.to_i} #{z + $3.to_i}"))
+ when /move (-?\d+) (-?\d+)/ then move_by($1.to_i, $2.to_i) && send_packet(MC::ChatMessage.new("moving to #{x + $1.to_i / 32.0} #{z + $2.to_i / 32.0}"))
+ when /dig (-?\d+) (-?\d+) (-?\d+) (\d) (\d)/ then dig($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i)
+ when /dig (-?\d+) (-?\d+) (-?\d+) (\d)/ then dig($1.to_i, $2.to_i, $3.to_i, $4.to_i)
+ when /slot (\d)/ then holding_slot($1.to_i)
+ when /place (-?\d+) (-?\d+) (-?\d+) (-?\d+) (\d)/ then place($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i)
+ when /place (-?\d+) (-?\d+) (-?\d+) (-?\d+)/ then place($1.to_i, $2.to_i, $3.to_i, $4.to_i)
+ when /place (-?\d+) (-?\d+) (-?\d+)/ then place($1.to_i, $2.to_i, $3.to_i, -1)
+ when /crouch/ then crouch
+ when /stand/ then stand
+ when /eat/ then eat && chat("nom nom")
+ end
+ end
+end
+
+host = 'li172-212.members.linode.com'
+nick = ARGV[0] || 'Bot'
+bot = Bot.new(nick)
+bot.connect(host)
+
+if bot.needs_session?
+ $stdout.write("Password: ")
+ pass = $stdin.readline.strip
+ session = MC::Session.new(nick, pass)
+ session.join_server(bot.connection_hash)
+end
+
+bot.send_packet(MC::LoginRequest.new(nick))
+#bot.send_packet(MC::KeepAlive.new)
+
+Mobs = Hash.new("Unknown")
+<<-EOT.split("\n").each { |l| tid, name = l.split(/\s+/); Mobs[tid.to_i] = name }
+50 Creeper
+51 Skeleton
+52 Spider
+53 Giant Zombie
+54 Zombie
+55 Slime
+56 Ghast
+57 Zombie Pigman
+58 Enderman
+59 Cave Spider
+60 Silverfish
+61 Blaze
+62 Magma Cube
+90 Pig
+91 Sheep
+92 Cow
+93 Hen
+94 Squid
+95 Wolf
+97 Snowman
+120 Villager
+-1 Player
+EOT
+
+def print_status(bot)
+ puts("Health:\t#{bot.health}\tFood:\t#{bot.food}\t#{bot.food_saturation}")
+ puts("Position:\t#{bot.x}, #{bot.y}, #{bot.z}\t#{bot.stance}")
+ puts("Rotation:\t#{bot.yaw} #{bot.pitch}")
+ puts("On ground") if bot.on_ground
+end
+
+def print_entity_count(entity_count)
+ puts(entity_count.collect { |(type, count)| "#{Mobs[type]}\t#{count}" }.join("\n"))
+end
+
+def print_players(players)
+ puts(players.
+ collect { |p| "#{p.name}\t#{p.entity_id}\t#{p.x}, #{p.y}, #{p.z}" }.
+ join("\n"))
+end
+
+def print_chat_messages(msgs)
+ msgs.each do |msg|
+ puts "#{msg}"
+ end
+end
+
+def reset_screen
+ print("\033[0;0f\033[2J")
+end
+
+packets = 0
+starting = Time.now
+ending = Time.now
+rate = 20
+
+begin
+ starting = Time.now
+ packets = 0
+
+ rate.times { |i|
+ i, o, e = IO.select([bot.socket], [bot.socket], nil, 10)
+ break unless o.include?(bot.socket)
+
+ bot.process_packet
+ packets += 1
+ }
+
+ ending = Time.now
+ packet_rate = 1.0 / (ending - starting)
+
+ reset_screen
+ puts "Packets: #{packets}\t#{packet_rate} packets per sec"
+ print_status(bot)
+ print_entity_count(bot.entity_count)
+ print_players(bot.named_entities)
+ print_chat_messages(bot.chat_messages[0, 5])
+end while true
26 bin/wiretap.rb
@@ -0,0 +1,26 @@
+require 'socket'
+
+s = TCPServer.new(25565)
+
+begin
+ client = s.accept
+
+ remote = TCPSocket.new('li172-212.members.linode.com', 25565)
+
+ begin
+ i, o, e = IO.select([remote, client], [remote, client], [], 10)
+
+ if i.include?(remote)
+ data = remote.recv(1024)
+ client.write(data) if data.size > 0
+ end
+
+ if i.include?(client)
+ data = client.recv(1024)
+ if data.size > 0
+ puts "#{Time.now}\t#{data.inspect}"
+ remote.write(data)
+ end
+ end
+ end until client.closed? || remote.closed?
+end while true
192 client_session.log
@@ -0,0 +1,192 @@
+"\002\000\n\000S\000n\000e\000a\000k\000y\000D\000e\000a\000n"
+"\001\000\000\000\021\000\n\000S\000n\000e\000a\000k\000y\000D\000e\000a\000n\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+"\r@!\000\000\000\000\000\000@P@\000\000\000\000\000@P\247\256\024\200\000\000@!\000\000\000\000\000\000\3034\000\000\000\000\000\000\000\v@!\000\000\000\000\000\000@P:\373~\217\\)@P\242\251\223\017\\)@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@P1\f,W\257\352@P\230\272@\327\257\352@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@P\"K6z\e\320@P\211\371J\372\e\320@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@P\016\321I0\270r@Pv\177]\260\270r@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@O\355m$\305\"\250@P^d\246\342\221T@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@O\264%\210T\331^@PA\300\330\252l\257@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@Oq\372.\241Gn@P \253+\320\243\267@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@O'\030\233p\233.@O\366t\304p\233.@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@N\323\255i\177\374\034@O\243\t\222\177\374\034@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@Nw\344O,\256\217@OG@x,\256\217@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@N\023\350#\005Z\207@N\343DL\005Z\207@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@M\247\342\340C\357\315@Nw?\tC\357\315@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@M3\375\2530\237 @N\003Y\3240\237 @!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@L\270`\325n]\302@M\207\274\376n]\302@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@L53\3421fd@M\004\220\v1fd@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@K\252\235\212`(%@Ly\371\263`(%@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@K\030\303\300\237\022\023@K\350\037\351\237\022\023@!\000\000\000\000\000\000\000"
+"\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@J\177\313\265G\247e@KO'\336G\247e@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@I\337\331\332KF\203@J\2576\003KF\203@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@I9\021\347\002\n\320@J\bn\020\002\n\320@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@H\213\226\333\346/\v@IZ\363\004\346/\v@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@G\327\213\006<T\#@H\246\347/<T\#@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@G\035\020\003\251\016M@G\354l,\251\016M@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@F\\F\305\264\030B@G+\242\356\264\030B@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@E\225O\2259\212\224@Fd\253\2769\212\224@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@D\310J\025\311s1@E\227\246>\311s1@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@C\365UH\366'P@D\304\261q\366'P@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@C\034\217\221\221\2508@C\353\353\272\221\2508@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@B>\026\266\332q\204@C\rr\337\332q\204@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@AZ\a\347\230\005\340@B)d\020\230\005\340@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@@p\177\275'\215h@A?\333\346'\215h@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@?\0034|\361\256\227@@P\366gx\327L@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@=\032\345\305\370\035,@>\271\236\027\370\035,@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@;(I+\000\340\377@<\307\001}\000\340\377@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@9+\223n\b\241\375@:\312K\300\b\241\375@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@7$\370B\355q\304@8\303\260\224\355q\304@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@5\024\252T\325\316K@6\263b\246\325\316K@!\000\000\000\000\000\000\000"
+"\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@2\372\333K{\373\215@4\231\223\235{\373\215@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@0\327\273\320^A\320@2vt\"^A\320@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@-V\367'\2516\241@0J3\345\324\233P@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@(\354\222\244\030\262\264@,*\003H\030\262\264@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@$p\245\257\324\214!@'\256\026S\324\214!@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@\037\307\024\037O\026H@# \372\263\247\213$@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@\026\213/y}\331\264@\035\006\020\301}\331\264@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000@\n\\\223\267nw$@\023\251+#\267;\222@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000?\355\210b\005F>\030@\004W\333\021Q\217\206@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000\277\367\257\216\370\260\270V?\301\337\261:z=P@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000\300\017O\307\256I\270\347\300\002Z\005\036I\270\347@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000\300\031\202T\377\207\355\303\300\023\as\267\207\355\303@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000\300!\275M\255s\236E\300\034\377\272\022\347<\212@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000\300&\310\017\025kR\260\300#\212\236qkR\260@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000\300+\341#\337{\035\214\300(\243\263;{\035\214@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000\3000\204!YS\214\212\300-\312\322\016\247\031\024@!\000\000\000\000\000\000\000"
+"\v@!\000\000\000\000\000\000\3003\036\221\326\276\270>\3001\177\331\204\276\270>@!\000\000\000\000\000\000\000"
+"\r\300Wh\000\000\000\000\000@I\000\000\000\000\000\000@I\317\\)\000\000\000@l\316\000\000\000\000\000C\035\262:B\0353@\000"
+"\r\300Wh\000\000\000\000\000@I\000\000\000\000\000\000@I\317\\)\000\000\000@l\316\000\000\000\000\000C\035\262:B\0353@\000"
+"\v\300Wh\000\000\000\000\000@H\365\366\375\036\270R@I\305S&\036\270R@l\316\000\000\000\000\000\000"
+"\v\300Wh\000\000\000\000\000@H\342\030X\257_\325@I\261t\201\257_\325@l\316\000\000\000\000\000\000"
+"\v\300Wh\000\000\000\000\000@H\304\226l\3647\240@I\223\362\225\3647\240@l\316\000\000\000\000\000\000\r\300Wh\000\000\000\000\000@I\000\000\000\000\000\000@I\317\\)\000\000\000@l\316\000\000\000\000\000C\035\262:B\0353@\000"
+"\v\300Wh\000\000\000\000\000@I\000\000\000\000\000\000@I\317\\)\000\000\000@l\316\000\000\000\000\000\000\n\001\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001\000\t_\200\317"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001\000\005&\255\255"
+"\n\001"
+"\n\001"
+"\n\001"
+"\fC\036\345mB\034\231\246\001"
+"\fC)\330\240B\025fs\001\000v \333 \n\001"
+"\fC?%mB\tfs\001"
+"\fCE%mB\a\000\r\001"
+"\fCO\362:A\376f\201\001"
+"\fCS\213\323A\372\314\350\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\fCP\330\240A\371\231\265\001"
+"\fCK\345mA\3613N\001"
+"\fCG\377\006A\352\000\e\001"
+"\fCE\230\240A\346f\201\001"
+"\fCC\313\323A\341\231\264\001"
+"\fCBr9A\336\000\032\001"
+"\fC=\313\322A\326\314\346\001"
+"\000^\f\203\362\n\001\n\001"
+"\fC+\177\005A\311\231\263\001"
+"\fC)\2628A\310f\200\001"
+"\fC#?\005A\3013M\001"
+"\fC\017\230\237A\267\231\263\001"
+"\fC\r\v\322A\2653M\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001\n\001"
+"\fC\022K\322A\262\314\347\001"
+"\fC\022\230\236A\262\314\347\001"
+"\n\001"
+"\022\000\000\006O\001\016\000\377\377\377\2412\000\000\000\344\003\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\fC\024ekA\261\231\264\001"
+"\022\000\000\006O\001\fC\025%kA\260f\201\001"
+"\022\000\000\006O\001\fC\026\v\322A\2573N\001"
+"\022\000\000\006O\001\fC\027\030\237A\256\000\e\001"
+"\000\334\373\340w"
+"\022\000\000\006O\001\fC\030%lA\253\231\265\001"
+"\022\000\000\006O\001\fC\030K\322A\253\231\265\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\016\002\377\377\377\2412\000\000\000\344\003\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\016\000\377\377\377\2412\000\000\000\343\003\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\fC\03128A\253\231\265\001"
+"\n\001"
+"\fC'\230\236A\266f\202\001"
+"\fC2\330\236A\276\314\351\001\000\0026\241\213"
+"\fC7\313\321A\303\231\265\001"
+"\fC:\177\004A\304\314\350\001"
+"\fC<K\321A\304\314\350\001"
+"\n\001"
+"\022\000\000\006O\001\016\000\377\377\377\2422\000\000\000\344\003\022\000\000\006O\001\fC<\345kA\304\314\350\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\016\002\377\377\377\2422\000\000\000\344\003\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001\000m\304\242_"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\016\000\377\377\377\2422\000\000\000\343\003\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\022\000\000\006O\001\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\000g\353\332|"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
134 client_session_dig.log
@@ -0,0 +1,134 @@
+"\002\000\n\000S\000n\000e\000a\000k\000y\000D\000e\000a\000n"
+"\001\000\000\000\021\000\n\000S\000n\000e\000a\000k\000y\000D\000e\000a\000n\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
+"\r\300WL\000\000\000\000\000@I\000\000\000\000\000\000@I\317\\)\000\000\000@l\334\000\000\000\000\000\302,\003rA\207\231\243\000"
+"\r\300WL\000\000\000\000\000@I\000\000\000\000\000\000@I\317\\)\000\000\000@l\334\000\000\000\000\000\302,\003rA\207\231\243\000"
+"\v\300WL\000\000\000\000\000@H\365\366\375\036\270R@I\305S&\036\270R@l\334\000\000\000\000\000\000\v\300WL\000\000\000\000\000@H\342\030X\257_\325@I\261t\201\257_\325@l\334\000\000\000\000\000\000\v\300WL\000\000\000\000\000@H\304\226l\3647\240@I\223\362\225\3647\240@l\334\000\000\000\000\000\000\v\300WL\000\000\000\000\000@H\235\242\222ap\344@Il\376\273ap\344@l\334\000\000\000\000\000\000\v\300WL\000\000\000\000\000@Hmm$\305\"\250@I<\311M\305\"\250@l\334\000\000\000\000\000\000\v\300WL\000\000\000\000\000@H4%\210T\331^@I\003\201\261T\331^@l\334\000\000\000\000\000\000"
+"\v\300WL\000\000\000\000\000@G\361\372.\241Gn@H\301VW\241Gn@l\334\000\000\000\000\000\000"
+"\v\300WL\000\000\000\000\000@G\247\030\233p\233.@Hvt\304p\233.@l\334\000\000\000\000\000\000"
+"\v\300WL\000\000\000\000\000@G\200\000\000\000\000\000@HO\\)\000\000\000@l\334\000\000\000\000\000\001\r\300WL\000\000\000\000\000@I\000\000\000\000\000\000@I\317\\)\000\000\000@l\334\000\000\000\000\000\302,\003rA\207\231\243\000"
+"\v\300WL\000\000\000\000\000@I\000\000\000\000\000\000@I\317\\)\000\000\000@l\334\000\000\000\000\000\000"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001\000\357\a\357\024"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\f\302$\320?A\2513<\001"
+"\f\302\027\235\vA\325\231\243\001"
+"\f\302\016\003qA\3673=\001"
+"\f\301\371:\025B\03538\001\000\243z\213\230"
+"\f\301\363:\025B7\000\004\001"
+"\f\302\0066\244BNfj\001"
+"\f\302\023i\330Ba\231\236\001"
+"\f\3021i\330B\201L\317\001"
+"\f\302i6\245B\214\2635\001"
+"\f\302\214N\206B\223\346h\001"
+"\f\302\250\316\207B\226\346h\001"
+"\f\302\277\233TB\225fh\001"
+"\f\302\341\316\207B\21635\001"
+"\f\302\377\316\207B\207L\317\001"
+"\f\303\023\032wB{fj\001"
+"\f\303\e\r\252Bs\000\004\001"
+"\f\303\e\315\252Bq37\001"
+"\f\303\034\332wBk\314\321\001"
+"\f\303\035\347DBd\000\004\001"
+"\f\303\036\r\252B`fk\001"
+"\f\303\036\r\252B]fk\001"
+"\f\303\036\r\252BY38\001"
+"\n\001"
+"\n\001"
+"\f\303\eZwBTfk\001"
+"\000\341\220<d"
+"\f\303\032\000\335BR\000\004\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\022\000\000\n\235\001\016\000\377\377\377\2431\000\000\000\345\001"
+"\022\000\000\n\235\001\n\001"
+"\022\000\000\n\235\001\n\001"
+"\000]\326E\030"
+"\022\000\000\n\235\001\n\001"
+"\022\000\000\n\235\001\n\001"
+"\022\000\000\n\235\001\n\001"
+"\022\000\000\n\235\001"
+"\n\001"
+"\022\000\000\n\235\001\n\001"
+"\022\000\000\n\235\001\n\001"
+"\022\000\000\n\235\001\n\001"
+"\016\002\377\377\377\2431\000\000\000\345\001"
+"\022\000\000\n\235\001\n\001"
+"\022\000\000\n\235\001\n\001"
+"\022\000\000\n\235\001\n\001"
+"\022\000\000\n\235\001\n\001"
+"\022\000\000\n\235\001\n\001"
+"\022\000\000\n\235\001\n\001"
+"\016\000\377\377\377\2430\000\000\000\345\001\022\000\000\n\235\001\n\001"
+"\022\000\000\n\235\001\n\001"
+"\022\000\000\n\235\001\n\001"
+"\022\000\000\n\235\001\n\001"
+"\022\000\000\n\235\001\n\001"
+"\n\001"
+"\n\001"
+"\n\001\000C\363\020\317"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\n\001"
+"\000N\270\3027"
+"\n\001"
+"\n\001"
+"\n\001"
+"\377\000\b\000Q\000u\000i\000t\000t\000i\000n\000g"
16 lib/mc.rb
@@ -0,0 +1,16 @@
+require 'active_support/core_ext'
+require 'logger'
+
+module MC
+ Version = 17
+
+ def self.logger
+ @logger ||= Logger.new($stderr, Logger::DEBUG)
+ end
+end
+
+require 'mc/session'
+require 'mc/packet'
+require 'mc/request'
+require 'mc/client'
+
206 lib/mc/client.rb
@@ -0,0 +1,206 @@
+module MC
+ autoload :Connection, 'mc/connection'
+
+ class Client
+ attr_reader :socket, :name, :entities
+ attr_accessor :connection_hash
+ attr_accessor :connection
+
+ def initialize(name)
+ @name = name
+ @connection = Connection.new
+ end
+
+ def close!
+ @connection.close!
+ @entities = Hash.new
+ end
+
+ def connect(host, port = 25565)
+ close!
+ @connection.connect(host, port)
+ do_handshake
+ end
+
+ def do_handshake
+ send_packet(MC::HandshakeRequest.new(name))
+ process_packet
+ end
+
+ def on_handshake(packet)
+ self.connection_hash = packet.connection_hash
+ end
+
+ def needs_session?
+ connection_hash != '-'
+ end
+
+ def process_packet
+ read_packet.handle(self)
+ end
+
+ def read_packet
+ @connection.read_packet
+ end
+
+ def send_packet(packet)
+ MC.logger.debug("Sending #{packet.inspect}")
+ @connection.send_packet(packet)
+ end
+
+ def socket
+ @connection.socket
+ end
+
+ attr_accessor :x, :y, :z, :stance, :yaw, :pitch, :on_ground
+
+ def x_absolute
+ x * 32
+ end
+
+ def y_absolute
+ y * 32
+ end
+
+ def z_absolute
+ z * 32
+ end
+
+ def stance_absolute
+ stance * 32
+ end
+
+ def change_stance_to(height)
+ self.stance = y + height
+ move_by(0, 0)
+ end
+
+ def move_by(*args)
+ if args.length == 2
+ dx, dz = args
+ send_packet(MC::PlayerPosition.new(x + dx.to_f, y, z + dz.to_f, stance, on_ground))
+ self.x += dx.to_f
+ self.z += dz.to_f
+ elsif args.length == 3
+ dx, dy, dz = args
+ send_packet(MC::PlayerPosition.new(x + dx.to_f, y + dy.to_f, z + dz.to_f, stance + dy.to_f, on_ground))
+ self.x += dx.to_f
+ self.y += dy.to_f
+ self.stance += dy.to_f
+ self.z += dz.to_f
+ end
+ end
+
+ attr_accessor :health, :food, :food_saturation
+
+ def on_update_health(packet)
+ self.health = packet.health
+ self.food = packet.food
+ self.food_saturation = packet.food_saturation
+ end
+
+ def on_keep_alive(packet)
+ send_packet(packet)
+ end
+
+ Mob = Struct.new(:entity_id, :x, :y, :z, :meta_data, :pitch, :yaw, :mob_type)
+
+ def on_mob_spawn(packet)
+ mob = Mob.new
+ mob.entity_id = packet.entity_id
+ mob.x = packet.x
+ mob.y = packet.y
+ mob.z = packet.z
+ mob.meta_data = packet.meta_data
+ mob.pitch = packet.pitch
+ mob.yaw = packet.yaw
+ mob.mob_type = packet.mob_type
+ @entities[packet.entity_id] = mob
+ end
+
+ NamedEntity = Struct.new(:entity_id, :name, :x, :y, :z, :rotation, :pitch, :current_item, :mob_type)
+
+ def on_named_entity_spawn(packet)
+ e = NamedEntity.new
+ e.entity_id = packet.entity_id
+ e.name = packet.name
+ e.x = packet.x
+ e.y = packet.y
+ e.z = packet.z
+ e.rotation = packet.rotation
+ e.pitch = packet.pitch
+ e.current_item = packet.current_item
+ e.mob_type = -1
+ @entities[packet.entity_id] = e
+ end
+
+ def on_entity_relative_move(packet)
+ e = @entities[packet.entity_id]
+ return if e.nil?
+
+ e.x += packet.dx
+ e.y += packet.dy
+ e.z += packet.dz
+ end
+
+ def on_destroy_entity(packet)
+ @entities.delete(packet.entity_id)
+ end
+
+ def chat_messages
+ @chat_messages ||= Array.new
+ end
+
+ def on_chat_message(packet)
+ chat_messages.unshift(packet.message)
+ end
+
+ def on_player_position_look(packet)
+ self.x = packet.x
+ self.y = packet.y
+ self.z = packet.z
+ self.yaw = packet.yaw
+ self.pitch = packet.pitch
+ self.stance = packet.stance
+ self.on_ground = packet.on_ground
+
+ send_packet(PlayerPositionAndLook.new(self.x, self.y, self.z, self.stance, self.yaw, self.pitch, self.on_ground))
+ end
+
+ def holding_slot(slot)
+ send_packet(HoldingChange.new(slot))
+ end
+
+ def eat
+ send_packet(PlayerBlockPlacement.new(-1, -1, -1, -1, -1, 0, 0))
+ end
+
+ def chat(msg)
+ send_packet(ChatMessage.new(msg))
+ end
+
+ def dig(dx, dy, dz, strikes = 50, face = MC::PlayerDigging::Face_Top)
+ send_packet(MC::ChatMessage.new("I am digging at #{x + dx} #{y + dy} #{z + dy}."))
+
+ # need to send a lot depending on the material and tool
+ strikes.times do
+ send_packet(MC::AnimationRequest.new(0, MC::AnimationRequest::SwingArm))
+ send_packet(MC::PlayerDigging.new(MC::PlayerDigging::Started, x + dx, y + dy, z + dz, face))
+ end
+
+ if strikes > 1
+ send_packet(MC::PlayerDigging.new(MC::PlayerDigging::Finished, x + dx, y + dy, z + dz, face))
+ send_packet(MC::AnimationRequest.new(0, MC::AnimationRequest::NoAnimation))
+ end
+ end
+
+ def place(dx, dy, dz, block_id = -1, direction = 2)
+ send_packet(MC::PlayerBlockPlacement.new(x + dx, y + dy, z + dz, direction, block_id))
+ end
+
+ def method_missing(mid, *args, &block)
+ MC.logger.debug("Method missing: #{mid}")
+ return super unless mid.to_s =~ /^on/
+ end
+ end
+end
36 lib/mc/connection.rb
@@ -0,0 +1,36 @@
+require 'socket'
+
+module MC
+ autoload :Request, 'mc/request'
+ autoload :HandshakeRequest, 'mc/request'
+ autoload :Parser, 'mc/parser'
+
+ class Connection
+ attr_reader :socket
+
+ def initialize
+ end
+
+ def close!
+ @socket.close if @socket
+ end
+
+ def connect(host, port = 25565)
+ close!
+
+ @socket = TCPSocket.new(host, port)
+ @parser = Parser.new(@socket)
+ end
+
+ def read_packet
+ @parser.read_packet
+ end
+
+ def send_packet(packet)
+ payload = packet.serialize
+ MC.logger.debug("Sent #{payload.inspect}")
+ @socket.write(payload)
+ @socket.flush
+ end
+ end
+end
703 lib/mc/packet.rb
@@ -0,0 +1,703 @@
+module MC
+ autoload :TypeIdFactory, 'mc/type_id_factory'
+
+ class Packet
+ include TypeIdFactory
+
+ class << self
+ def packet_id(id)
+ register(id)
+
+ define_method(:packet_id) do
+ id
+ end
+ end
+
+ def create(packet_id, parser)
+ p = Factory.create(packet_id)
+ p.deserialize(parser)
+ p
+ end
+
+ def deserialize(parser)
+ p = self.new
+ p.deserialize(parser)
+ p
+ end
+ end
+
+ def serialize
+ [ packet_id ].pack('C')
+ end
+
+ def deserialize(parser)
+ end
+
+ def handle(client)
+ client.send("on_#{self.class.name.underscore}", self)
+ end
+ end
+
+ class KickPacket < Packet
+ attr_accessor :reason
+
+ packet_id 0xFF
+
+ def deserialize(parser)
+ self.reason = parser.read_string
+ end
+
+ def handle(client)
+ client.on_kick(self)
+ end
+ end
+
+ class KeepAlive < Packet
+ packet_id 0x00
+
+ attr_accessor :keep_alive_id
+
+ def initialize
+ self.keep_alive_id = rand(0xFFFFFFFF)
+ end
+
+ def serialize
+ super + [ keep_alive_id ].pack('N')
+ end
+
+ def deserialize(parser)
+ self.keep_alive_id = parser.read_ulong
+ end
+
+ def handle(client)
+ client.on_keep_alive(self)
+ end
+ end
+
+ class LoginReply < Packet
+ packet_id 0x01
+ attr_accessor :entity_id, :map_seed, :server_mode, :dimension, :difficulty, :world_height, :max_players
+
+ def deserialize(parser)
+ self.entity_id = parser.read_ulong
+ parser.read_bytes(2)
+ self.map_seed = parser.read_ulonglong
+ self.server_mode = parser.read_ulong
+ self.dimension = parser.read_char
+ self.difficulty = parser.read_char
+ self.world_height = parser.read_byte
+ self.max_players = parser.read_byte
+
+ # data = io.read(24)
+ # puts data.inspect
+ # self.entity_id, self.map_seed, self.server_mode, self.dimension, self.difficulty, self.world_height, self.max_players = data.unpack('NxxQNccCC')
+ end
+
+ def handle(client)
+ client.on_login_reply(self)
+ end
+ end
+
+ class ChatMessage < Packet
+ packet_id 0x03
+ attr_accessor :message
+
+ def initialize(message = nil)
+ self.message = message
+ end
+
+ def serialize
+ super + String16.serialize(message)
+ end
+
+ def deserialize(parser)
+ self.message = parser.read_string
+ end
+
+ def handle(client)
+ client.on_chat_message(self)
+ end
+ end
+
+ class NamedEntitySpawn < Packet
+ packet_id 0x14
+ attr_accessor :entity_id, :name, :x, :y, :z, :rotation, :pitch, :current_item
+
+ def deserialize(parser)
+ self.entity_id = parser.read_ulong
+ self.name = parser.read_string
+ self.x = parser.read_long
+ self.y = parser.read_long
+ self.z = parser.read_long
+ self.rotation = parser.read_char
+ self.pitch = parser.read_char
+ self.current_item = parser.read_short
+ end
+
+ def handle(client)
+ client.on_named_entity_spawn(self)
+ end
+ end
+
+ class HandshakeReply < Packet
+ packet_id 0x02
+ attr_accessor :connection_hash
+
+ def deserialize(parser)
+ self.connection_hash = parser.read_string
+ end
+
+ def serialize
+ super + String16.serialize(connection_hash)
+ end
+
+ def handle(client)
+ client.on_handshake(self)
+ end
+ end
+
+ class Metadata
+ def self.deserialize(parser)
+ data = Hash.new
+
+ while (x = parser.read_byte) && x != 127
+ #puts x.inspect
+ data[x & 0x1F] = case (x >> 5)
+ when 0 then parser.read_byte
+ when 1 then parser.read_short
+ when 2 then parser.read_long
+ when 3 then parser.read_float
+ when 4 then parser.read_string
+ when 5 then ItemStack.deserialize(parser)
+ when 6 then EntityInfo.deserialize(parser)
+ end
+ end
+
+ data
+ end
+ end
+
+ class MobSpawn < Packet
+ packet_id 0x18
+ attr_accessor :entity_id, :mob_type, :x, :y, :z, :yaw, :pitch, :meta_data
+
+ def deserialize(parser)
+ self.entity_id = parser.read_ulong
+ self.mob_type = parser.read_byte
+ self.x = parser.read_long
+ self.y = parser.read_long
+ self.z = parser.read_long
+ self.yaw = parser.read_char
+ self.pitch = parser.read_char
+
+ # data = io.read(19)
+ # self.entity_id, self.mob_type, self.x, self.y, self.z, self.yaw, self.pitch = data.unpack('NcNNNcc')
+ # self.x = self.x - MAX_INT if self.x > MAX_SIGNED_INT
+ # self.y = self.y - MAX_INT if self.y > MAX_SIGNED_INT
+ # self.z = self.z - MAX_INT if self.z > MAX_SIGNED_INT
+ self.meta_data = parser.read_metadata
+ end
+
+ def block_position
+ [ x / 32.0, y / 32.0, z / 32.0 ]
+ end
+
+ def handle(client)
+ client.on_mob_spawn(self)
+ end
+ end
+
+ class AddObject < Packet
+ packet_id 0x17
+ attr_accessor :entity_id, :entity_type, :x, :y, :z, :thrower_id, :a, :b, :c
+
+ def deserialize(parser)
+ self.entity_id = parser.read_ulong
+ self.entity_type = parser.read_byte
+ self.x = parser.read_long
+ self.y = parser.read_long
+ self.z = parser.read_long
+ self.thrower_id = parser.read_long
+ if thrower_id > 0
+ self.a = parser.read_short
+ self.b = parser.read_short
+ self.c = parser.read_short
+ end
+
+ # self.entity_id, self.entity_type, self.x, self.y, self.z, self.thrower_id = io.read(21).unpack('NcNNNN')
+ # self.a, self.b, self.c = io.read(6).unpack('nnn') if self.thrower_id > 0
+ end
+ end
+
+ class ItemStack
+ attr_accessor :id, :count, :damage
+
+ def self.deserialize(parser)
+ r = self.new
+ r.deserialize(parser)
+ r
+ end
+
+ def deserialize(parser)
+ self.id = parser.read_short
+ self.count = parser.read_byte
+ self.damage = parser.read_short
+ #self.id, self.count, self.damage = io.read(5).unpack('ncn')
+ end
+ end
+
+ class EntityInfo
+ attr_accessor :a, :b, :c
+
+ def self.deserialize(parser)
+ r = self.new
+ r.deserialize(parser)
+ r
+ end
+
+ def deserialize(parser)
+ self.a = parser.read_long
+ self.b = parser.read_long
+ self.c = parser.read_long
+ #self.a, self.b, self.c = io.read(4 * 3).unpack('NNN')
+ end
+ end
+
+ class EntityMetadata < Packet
+ packet_id 0x28
+ attr_accessor :entity_id, :meta_data
+
+ def deserialize(parser)
+ self.entity_id = parser.read_ulong
+ self.meta_data = parser.read_metadata
+ end
+ end
+
+ class DestroyEntity < Packet
+ packet_id 0x1D
+ attr_accessor :entity_id
+
+ def deserialize(parser)
+ self.entity_id = parser.read_ulong
+ end
+
+ def handle(client)
+ client.on_destroy_entity(self)
+ end
+ end
+
+ class EntityVelocity < Packet
+ packet_id 0x1C
+ attr_accessor :entity_id, :x, :y, :z
+
+ def deserialize(parser)
+ self.entity_id = parser.read_ulong
+ self.x = parser.read_short
+ self.y = parser.read_short
+ self.z = parser.read_short
+ #self.entity_id, self.x, self.y, self.z = io.read(10).unpack('Nnnn')
+ end
+ end
+
+ class EntityRelativeMove < Packet
+ packet_id 0x1F
+ attr_accessor :entity_id, :dx, :dy, :dz
+
+ def deserialize(parser)
+ self.entity_id = parser.read_ulong
+ self.dx = parser.read_char
+ self.dy = parser.read_char
+ self.dz = parser.read_char
+ #self.entity_id, self.dx, self.dy, self.dz = io.read(7).unpack('Nccc')
+ end
+
+ def handle(client)
+ client.on_entity_relative_move(self)
+ end
+ end
+
+ class EntityLookRelativeMove < Packet
+ packet_id 0x21
+ attr_accessor :entity_id, :dx, :dy, :dz, :yaw, :pitch
+
+ def deserialize(parser)
+ self.entity_id = parser.read_ulong
+ self.dx = parser.read_char
+ self.dy = parser.read_char
+ self.dz = parser.read_char
+ self.yaw = parser.read_char
+ self.pitch = parser.read_char
+ #self.entity_id, self.dx, self.dy, self.dz, self.yaw, self.pitch = io.read(9).unpack('Nccccc')
+ end
+
+ def handle(client)
+ client.on_entity_relative_move(self)
+ end
+ end
+
+ class EntityStatus < Packet
+ packet_id 0x26
+ attr_accessor :entity_id, :status
+
+ def deserialize(parser)
+ self.entity_id = parser.read_ulong
+ self.status = parser.read_char
+ #self.entity_id, self.status = io.read(5).unpack('Nc')
+ end
+ end
+
+ class EntityTeleport < Packet
+ packet_id 0x22
+ attr_accessor :entity_id, :x, :y, :z, :yaw, :pitch
+
+ def deserialize(parser)
+ self.entity_id = parser.read_ulong
+ self.x = parser.read_long
+ self.y = parser.read_long
+ self.z = parser.read_long
+ self.yaw = parser.read_char
+ self.pitch = parser.read_char
+ #self.entity_id, self.x, self.y, self.z, self.yaw, self.pitch = io.read(18).unpack('NNNNcc')
+ end
+ end
+
+ class Experience < Packet
+ packet_id 0x2B
+ attr_accessor :current, :level, :total
+
+ def deserialize(parser)
+ self.current = parser.read_byte
+ self.level = parser.read_byte
+ self.total = parser.read_short
+ end
+ end
+
+ class PreChunk < Packet
+ packet_id 0x32
+ attr_accessor :x, :z, :mode
+
+ def deserialize(parser)
+ self.x = parser.read_long
+ self.z = parser.read_long
+ self.mode = parser.read_char
+ #self.x, self.z, self.mode = io.read(9).unpack('NNc')
+ end
+ end
+
+ class MapChunk < Packet
+ packet_id 0x33
+ attr_accessor :x, :y, :z, :size_x, :size_y, :size_z, :data_size, :data
+
+ def deserialize(parser)
+ self.x = parser.read_long
+ self.y = parser.read_short
+ self.z = parser.read_long
+ self.size_x = parser.read_byte
+ self.size_y = parser.read_byte
+ self.size_z = parser.read_byte
+ self.data_size = parser.read_ulong
+ #self.x, self.y, self.z, self.size_x, self.size_y, self.size_z, self.data_size = io.read(17).unpack('NnNcccN')
+ self.data = parser.read_bytes(self.data_size)
+ end
+ end
+
+ class BlockAction < Packet
+ packet_id 0x36
+ attr_accessor :x, :y, :z, :byte1, :byte2
+
+ def deserialize(parser)
+ self.x = parser.read_long
+ self.y = parser.read_short
+ self.z = parser.read_long
+ self.byte1 = parser.read_byte
+ self.byte2 = parser.read_byte
+ #self.x, self.y, self.z, self.byte1, self.byte2 = io.read(12).unpack('NnNcc')
+ end
+ end
+
+ class BlockChange < Packet
+ packet_id 0x35
+ attr_accessor :x, :y, :z, :block_type, :meta_data
+
+ def deserialize(parser)
+ self.x = parser.read_long
+ self.y = parser.read_char
+ self.z = parser.read_long
+ self.block_type = parser.read_byte
+ self.meta_data = parser.read_byte
+ #self.x, self.y, self.z, self.block_type, self.meta_data = io.read(11).unpack('NcNcc')
+ end
+ end
+
+ class MultiBlockChange < Packet
+ packet_id 0x34
+ attr_accessor :x, :y, :size, :coordinates, :type, :meta_data
+
+ def deserialize(parser)
+ self.x = parser.read_long
+ self.y = parser.read_long
+ self.size = parser.read_short
+ self.coordinates = parser.read_shorts(self.size)
+ self.type = parser.read_bytes(self.size)
+ self.meta_data = parser.read_bytes(self.size)
+ #self.x, self.y, self.size = io.read(10).unpack('NNn')
+ #self.coordinates = io.read(self.size * 2).unpack("n#{self.size}")
+ #self.type = io.read(self.size).unpack("C#{self.size}")
+ #self.meta_data = io.read(self.size).unpack("C#{self.size}")
+ end
+ end
+
+ class PickupSpawn < Packet
+ packet_id 0x15
+ attr_accessor :entity_id, :item_id, :count_id, :damage, :x, :y, :z, :rotation, :pitch, :roll
+
+ def deserialize(parser)
+ self.entity_id = parser.read_ulong
+ self.item_id = parser.read_ushort
+ self.count_id = parser.read_char
+ self.damage = parser.read_short
+ self.x = parser.read_long
+ self.y = parser.read_long
+ self.z = parser.read_long
+ self.rotation = parser.read_char
+ self.pitch = parser.read_char
+ self.roll = parser.read_char
+
+ #self.entity_id, self.item_id, self.count_id, self.damage, self.x, self.y, self.z, self.rotation, self.pitch, self.roll = io.read(24).unpack('NncnNNNccc')
+ end
+ end
+
+ class Animation < Packet
+ packet_id 0x12
+ attr_accessor :entity_id, :animation
+
+ def deserialize(parser)
+ self.entity_id = parser.read_ulong
+ self.animation = parser.read_byte
+ #self.entity_id, self.animation = io.read(5).unpack('Nc')
+ end
+ end
+
+ class EntityAction < Packet
+ packet_id 0x13
+ attr_accessor :entity_id, :action_id
+
+ Crouch = 1
+ Stand = 2
+ LeaveBed = 3
+ Start_Sprinting = 4
+ Stop_Sprinting = 5
+
+ def initialize(entity_id = nil, action_id = Stand)
+ self.entity_id = entity_id
+ self.action_id = action_id
+ end
+
+ def deserialize(parser)
+ self.entity_id = parser.read_ulong
+ self.action_id = parser.read_byte
+ #self.entity_id, self.action_id = io.read(5).unpack('Nc')
+ end
+
+ def serialize
+ super + [ entity_id, action_id ].pack('Nc')
+ end
+ end
+
+ class CollectItem < Packet
+ packet_id 0x16
+ attr_accessor :collected, :collector
+
+ def deserialize(parser)
+ self.collected = parser.read_ulong
+ self.collector = parser.read_ulong
+ end
+ end
+
+ class SoundEffect < Packet
+ packet_id 0x3D
+ attr_accessor :effect_id, :x, :y, :z, :data
+
+ def deserialize(parser)
+ self.effect_id = parser.read_ulong
+ self.x = parser.read_long
+ self.y = parser.read_char
+ self.z = parser.read_long
+ self.data = parser.read_long
+ #self.effect_id, self.x, self.y, self.z, self.data = io.read(17).unpack('NNcNN')
+ end
+ end
+
+ class NewState < Packet
+ packet_id 0x46
+ attr_accessor :reason, :game_mode
+
+ def deserialize(parser)
+ self.reason = parser.read_byte
+ self.game_mode = parser.read_byte
+ end
+
+ def reason_string
+ [ "Invalid bed", "Begin raining", "End raining", "Change game mode" ][reason]
+ end
+ end
+
+ class ThunderBolt < Packet
+ packet_id 0x47
+ attr_accessor :entity_id, :unknown, :x, :y, :z
+
+ def deserialize(parser)
+ self.entity_id = parser.read_ulong
+ self.unknown = parser.read_byte
+ self.x = parser.read_long
+ self.y = parser.read_long
+ self.z = parser.read_long
+ end
+ end
+
+ class IncrementStatistic < Packet
+ packet_id 0xC8
+ attr_accessor :statistic_id, :amount
+
+ def deserialize(parser)
+ self.statistic_id = parser.read_ulong
+ self.amount = parser.read_char
+ #self.statistic_id, self.amount = io.read(5).unpack('Nc')
+ end
+ end
+
+ class PlayerListItem < Packet
+ packet_id 0xC9
+ attr_accessor :player_name, :online, :ping
+
+ def packet_id
+ 0xC9
+ end
+
+ def deserialize(parser)
+ self.player_name = parser.read_string
+ self.online = parser.read_byte
+ self.ping = parser.read_ushort
+ #self.player_name = String16.deserialize(io)
+ #self.online, self.ping = io.read(3).unpack('cn')
+ end
+ end
+
+ class UpdateHealth < Packet
+ packet_id 0x08
+ attr_accessor :health, :food, :food_saturation
+
+ def deserialize(parser)
+ self.health = parser.read_short
+ self.food = parser.read_short
+ self.food_saturation = parser.read_float
+ end
+
+ def handle(client)
+ client.on_update_health(self)
+ end
+ end
+
+ class PlayerPositionLookResponse < Packet
+ packet_id 0x0D
+ attr_accessor :x, :stance, :y, :z, :yaw, :pitch, :on_ground
+
+ def deserialize(parser)
+ self.x = parser.read_double_float_big
+ self.stance = parser.read_double_float_big
+ self.y = parser.read_double_float_big
+ self.z = parser.read_double_float_big
+ self.yaw = parser.read_float_big
+ self.pitch = parser.read_float_big
+ self.on_ground = parser.read_char
+ #self.x, self.stance, self.y, self.z, self.yaw, self.pitch, self.on_ground = io.read(41).unpack('GGGGggc')
+ end
+
+ def handle(client)
+ client.on_player_position_look(self)
+ end
+ end
+
+ class TimeUpdate < Packet
+ packet_id 0x04
+ attr_accessor :time
+
+ def deserialize(parser)
+ self.time = parser.read_double_float_big
+ #self.time = io.read(8).unpack('Q')
+ end
+ end
+
+ class SpawnPosition < Packet
+ packet_id 0x06
+ attr_accessor :x, :y, :z
+
+ def deserialize(parser)
+ self.x = parser.read_long
+ self.y = parser.read_long
+ self.z = parser.read_long
+ #self.x, self.y, self.z = io.read(12).unpack('NNN')
+ end
+ end
+
+ class EntityEquipment < Packet
+ packet_id 0x05
+ attr_accessor :entity_id, :slot, :item_id, :damage
+
+ def deserialize(parser)
+ self.entity_id = parser.read_ulong
+ self.slot = parser.read_short
+ self.item_id = parser.read_short
+ self.damage = parser.read_short
+ #self.entity_id, self.slot, self.item_id, self.damage = io.read(10).unpack('Nnnn')
+ end
+ end
+
+ class SetSlot < Packet
+ packet_id 0x67
+ attr_accessor :window_id, :slot, :item_id, :item_count, :item_uses
+
+ def deserialize(parser)
+ self.window_id = parser.read_char
+ self.slot = parser.read_short
+ self.item_id = parser.read_short
+
+ if item_id > -1
+ self.item_count = parser.read_char
+ self.item_uses = parser.read_short
+ end
+ #self.window_id, self.slot, self.item_id = io.read(5).unpack('css')
+ #self.item_count, self.item_uses = io.read(3).unpack('cs') if self.item_id > -1
+ end
+ end
+
+ class WindowItems < Packet
+ packet_id 0x68
+ attr_accessor :window_id, :count, :slots
+
+ Slot = Struct.new(:item_id, :count, :uses)
+
+ def deserialize(parser)
+ self.window_id = parser.read_byte
+ self.count = parser.read_short
+ #self.window_id, self.count = io.read(3).unpack('cn')
+ MC.logger.debug("Reading #{count} slots")
+
+ self.slots = Hash.new
+ self.count.times do |i|
+ item_id = parser.read_short
+
+ if item_id != -1
+ slot_count = parser.read_byte
+ slot_uses = parser.read_ushort
+ self.slots[i] = Slot.new(item_id, slot_count, slot_uses)
+ MC.logger.debug("Added slot #{i}\t#{slots[i].inspect}")
+ end
+
+ #item_id = io.read(2).unpack('s')[0]
+ #count, uses = io.read(3).unpack('cs') if item_id != -1
+ end
+ end
+ end
+
+end
87 lib/mc/parser.rb
@@ -0,0 +1,87 @@
+module MC
+ autoload :String16, 'mc/string16'
+ autoload :Packet, 'mc/packet'
+
+ MAX_SIGNED_INT = (2 ** 31) - 1
+ MAX_INT = (2 ** 32)
+
+ MAX_SIGNED_SHORT = (2 ** 15) - 1
+ MAX_SHORT = (2 ** 16)
+
+ class Parser
+ def initialize(io)
+ @io = io
+ end
+
+ def read_packet
+ data = read_byte
+ return unless data
+
+ packet = Packet.create(data, self)
+ MC.logger.debug("Packet: #{packet.inspect}")
+ packet
+ end
+
+ def read_string
+ String16.deserialize(@io)
+ end
+
+ def read_short
+ i = @io.read(2).unpack('n')[0]
+ i = i - MAX_SHORT if i > MAX_SIGNED_SHORT
+ i
+ end
+
+ def read_shorts(n)
+ @io.read(n * 2).unpack("n#{n}")
+ end
+
+ def read_ushort
+ s = @io.read(2).unpack('n')[0]
+ s = s - (2 ** 16) if s > (2 ** 15)
+ s
+ end
+
+ def read_long
+ i = @io.read(4).unpack('N')[0]
+ i = i - MAX_INT if i > MAX_SIGNED_INT
+ i
+ end
+
+ def read_ulong
+ @io.read(4).unpack('N')[0]
+ end
+
+ def read_ulonglong
+ @io.read(8).unpack('Q')[0]
+ end
+
+ def read_char
+ @io.read(1).unpack("c")[0]
+ end
+
+ def read_byte
+ @io.read(1).unpack("C")[0]
+ end
+
+ def read_bytes(n)
+ @io.read(n).unpack("C#{n}")
+ end
+
+ def read_float
+ @io.read(4).unpack('e')[0]
+ end
+
+ def read_float_big
+ @io.read(4).unpack('g')[0]
+ end
+
+ def read_double_float_big
+ @io.read(8).unpack('G')[0]
+ end
+
+ def read_metadata
+ Metadata.deserialize(self)
+ end
+ end
+end
212 lib/mc/request.rb
@@ -0,0 +1,212 @@
+module MC
+ autoload :TypeIdFactory, 'mc/type_id_factory'
+
+ class Request
+ include TypeIdFactory
+
+ class << self
+ def packet_id(id)
+ register(id)
+
+ define_method(:packet_id) do
+ id
+ end
+ end
+
+ def deserialize(parser)
+ i = self.new
+ i.deserialize(parser)
+ i
+ end
+ end
+
+ def serialize
+ [ packet_id ].pack('C')
+ end
+ end
+
+ class LoginRequest < Request
+ packet_id 0x01
+
+ attr_accessor :protocol_version, :user_name, :unused
+
+ def initialize(name = 'MC Bot')
+ self.protocol_version = 17
+ self.user_name = name
+ end
+
+ def serialize
+ super + [ protocol_version ].pack('N') + String16.serialize(user_name) + Array.new(16, 0).pack('C16')
+ end
+
+ def deserialize(parser)
+ self.protocol_version = parser.read_ulong
+ self.user_name = parser.read_string
+ self.unused = parser.read_bytes(16)
+ end
+ end
+
+ class HandshakeRequest < Request
+ packet_id 0x02
+ attr_accessor :user_name
+
+ def initialize(user_name = 'MC Bot')
+ self.user_name = user_name
+ end
+
+ def serialize
+ super + String16.serialize(user_name)
+ end
+ end
+
+ class PlayerOnGround < Request
+ packet_id 0x0A
+ attr_accessor :on_ground
+
+ def initialize(on_ground = true)
+ self.on_ground = on_ground
+ end
+
+ def serialize
+ super + [ on_ground ? 1 : 0 ].pack('c')
+ end
+ end
+
+ class PlayerPosition < Request
+ packet_id 0x0B
+ attr_accessor :x, :y, :z, :stance, :on_ground
+
+ def initialize(x, y, z, stance, on_ground)
+ self.x = x
+ self.y = y
+ self.z = z
+ self.stance = stance
+ self.on_ground = on_ground
+ end
+
+ def serialize
+ super + [ x.to_f, y.to_f, stance.to_f, z.to_f, on_ground ? 1 : 0 ].pack('GGGGc')
+ end
+ end
+
+ class PlayerLook < Request
+ packet_id 0x0C
+ attr_accessor :yaw, :pitch, :on_ground
+
+ def initialize(yaw = 0.0, pitch = 0.0, on_ground = true)
+ self.yaw = yaw
+ self.pitch = pitch
+ self.on_ground = on_ground
+ end
+
+ def serialize
+ super + [ self.yaw.to_f, self.pitch.to_f, self.on_ground ? 1 : 0 ].pack('ggc')
+ end
+ end
+
+ class PlayerPositionAndLook < Request
+ packet_id 0x0D
+ attr_accessor :x, :y, :z, :stance, :yaw, :pitch, :on_ground
+
+ def initialize(x, y, z, stance, yaw, pitch, on_ground)
+ self.x = x
+ self.y = y
+ self.z = z
+ self.stance = stance
+ self.yaw = yaw
+ self.pitch = pitch
+ self.on_ground = on_ground
+ end
+
+ def serialize
+ super + [ self.x.to_f, self.y.to_f, self.stance.to_f, self.z.to_f, self.yaw.to_f, self.pitch.to_f, self.on_ground ? 1 : 0].pack('GGGGggc')
+ end
+ end
+
+ class PlayerDigging < Request
+ packet_id 0x0E
+ attr_accessor :status, :x, :y, :z, :face
+
+ Started = 0
+ Finished = 2
+ Dropped = 4
+ ShootArrow = 5
+
+ Face_Bottom = 0
+ Face_Top = 1
+ Face_North = 3
+ Face_South = 2
+ Face_East = 5
+ Face_West = 4
+
+ def initialize(status, x, y, z, face)
+ self.status = status
+ self.x = x
+ self.y = y
+ self.z = z
+ self.face = face
+ end
+
+ def serialize
+ super + [ status.to_i, x.to_i, y.to_i, z.to_i, face.to_i ].pack("cNcNc")
+ end
+ end
+
+ class PlayerBlockPlacement < Request
+ packet_id 0x0F
+ attr_accessor :x, :y, :z, :direction, :block_id, :amount, :damage
+
+ def initialize(x, y, z, direction, block_id, amount = 0, damage = 0)
+ self.x = x
+ self.y = y
+ self.z = z
+ self.direction = direction
+ self.block_id = block_id
+ self.amount = amount
+ self.damage = damage
+ end
+
+ def serialize
+ if block_id >= 0
+ super + [ x, y, z, direction, block_id, amount, damage ].pack('NcNcncn')
+ else
+ super + [ x, y, z, direction, block_id ].pack('NcNcn')
+ end
+ end
+ end
+
+ class HoldingChange < Request
+ packet_id 0x10
+ attr_accessor :slot
+
+ def initialize(slot)
+ self.slot = slot
+ end
+
+ def serialize
+ super + [ slot ].pack('n')
+ end
+ end
+
+ class AnimationRequest < Request
+ packet_id 0x12
+ attr_accessor :entity_id, :animation
+
+ NoAnimation = 0
+ SwingArm = 1
+ Damage = 2
+ LeaveBed = 3
+ EatFood = 5
+ Crouch = 104
+ Uncrouch = 105
+
+ def initialize(entity_id, animation)
+ self.entity_id = entity_id
+ self.animation = animation
+ end
+
+ def serialize
+ super + [ entity_id, animation ].pack('Nc')
+ end
+ end
+end
29 lib/mc/session.rb
@@ -0,0 +1,29 @@
+require 'net/https'
+
+module MC
+ class Session
+ attr_reader :version, :ticket, :user_name, :session_id
+
+ def initialize(nick, password)
+ http = https_to("https://login.minecraft.net/")
+
+ req = Net::HTTP::Post.new("/")
+ req.set_form_data({ :user => nick, :password => password, :version => Version })
+ res = http.request(req)
+ @version, @ticket, @user_name, @session_id = res.body.rstrip.split(':')
+ end
+
+ def join_server(server_hash)
+ http = Net::HTTP.new("session.minecraft.net")
+ req = Net::HTTP::Get.new("/game/joinserver.jsp?user=#{user_name}&sessionId=#{session_id}&serverId=#{server_hash}")
+ res = http.request(req)
+ end
+
+ def https_to(uri)
+ uri = URI.parse(uri)
+ http = Net::HTTP.new(uri.host, uri.port)
+ http.use_ssl = true
+ http
+ end
+ end
+end
15 lib/mc/string16.rb
@@ -0,0 +1,15 @@
+module MC
+ class String16
+ def self.serialize(str)
+ [ str.length ].pack('n') + str.chars.collect { |c| c[0] }.pack("n#{str.length}")
+ end
+
+ def self.deserialize(io)
+ data = io.read(2)
+ len = data.unpack('n')[0]
+
+ data = io.read(len * 2)
+ data.unpack("n#{len}").inject("") { |a, c| a << c }
+ end
+ end
+end
37 lib/mc/type_id_factory.rb
@@ -0,0 +1,37 @@
+require 'active_support/core_ext'
+
+module MC
+ module TypeIdFactory
+ def self.included(base)
+ base.instance_eval <<-EOT
+ Factory = TypeIdFactory::Worker.new
+
+ def types
+ Factory.types
+ end
+
+ def register(id)
+ Factory.register(id, self)
+ end
+
+ def create(id)
+ Factory.create(id)
+ end
+ EOT
+ end
+
+ class Worker
+ def types
+ @types ||= Hash.new { |h, key| raise "Bad type id: %#x" % [ key ] }
+ end
+
+ def register(id, klass)
+ types[id] = klass
+ end
+
+ def create(type_id)
+ types[type_id].new
+ end
+ end
+ end
+end
0 lib/mc_bot.rb
No changes.
116 test/mc_bot_test.rb
@@ -0,0 +1,116 @@
+require 'mc_bot'
+require 'stringio'
+
+describe MC::KickPacket do
+ describe '#deserialize' do
+ let(:data) { "\000\016\000P\000r\000o\000t\000o\000c\000o\000l\000 \000e\000r\000r\000o\000r" }
+ let(:io) { StringIO.new(data) }
+
+ before { subject.deserialize(io) }
+
+ it "has the reason 'Protocol error'" do
+ subject.reason.should == 'Protocol error'
+ end
+ end
+end
+
+describe MC::LoginRequest do
+ describe '#deserialize' do
+ let(:data) { "\000\000\000\021\000\n\000S\000n\000e\000a\000k\000y\000D\000e\000a\000n\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000" }
+
+ subject { described_class.deserialize(StringIO.new(data)) }
+
+ it "has protocol version 17" do
+ subject.protocol_version.should == 17
+ end
+
+ it "has SneakyDean as the user" do
+ subject.user_name.should == 'SneakyDean'
+ end
+
+ it "has 16 bytes unused" do
+ subject.unused.length.should == 16
+ end
+ end
+
+ describe '#serialize' do
+ context 'empty name' do
+ let(:packet) { described_class.new("") }
+ subject { packet.serialize }
+
+ it "is 23 bytes long" do
+ subject.length.should == 23
+ end
+ end
+
+ context "client data" do
+ let(:data) { "\001\000\000\000\021\000\n\000S\000n\000e\000a\000k\000y\000D\000e\000a\000n\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000" }
+
+ subject { described_class.deserialize(StringIO.new(data[1..-1])) }
+
+ it "is the same" do
+ subject.serialize.should == data
+ end
+ end
+ end
+end
+
+describe MC::LoginReply do
+ describe '#deserialize' do
+ let(:data) { "\000\003\266\253\000\000\352\273[7\375\271\253\266\000\000\000\000\000\001\200\024\006\000" }
+
+ subject { described_class.deserialize(StringIO.new(data)) }
+
+ it "is survival" do
+ subject.server_mode.should == 0
+ end
+
+ it "is not in the nether" do
+ subject.dimension.should == 0
+ end
+
+ it "is easy" do
+ subject.difficulty.should == 1
+ end
+
+ it "has 128 world height" do
+ subject.world_height.should == 128
+ end
+
+ it "has 20 players" do
+ subject.max_players.should == 20
+ end
+ end
+end
+
+describe MC::MobSpawn do
+ describe '.deserialize' do
+ let(:data) { "\000\003\264\3434\377\377\361\220\000\000\005\200\000\000\v\360z\000" }
+
+ subject { described_class.deserialize(StringIO.new(data)) }
+
+ it "has 242915 as entity id" do
+ subject.entity_id.should == 242915
+ end
+
+ it "is a spider" do
+ subject.mob_type.should == 52
+ end
+
+ it "has a position" do
+ subject.x.should == 4294963600
+ subject.y.should == 1408
+ subject.z.should == 3056
+ end
+
+ it "is yawed" do
+ subject.yaw.should == 122
+ end
+
+ it "is pitched" do
+ subject.pitch.should == 0
+ end
+
+ it "has metadata"
+ end
+end

0 comments on commit 9dd2749

Please sign in to comment.
Something went wrong with that request. Please try again.