Skip to content

Instantly share code, notes, and snippets.

@NitroXenon
Forked from mid-kid/minecraft-chat-client.py
Last active August 29, 2015 14:23
Show Gist options
  • Save NitroXenon/6735f7079027b2758693 to your computer and use it in GitHub Desktop.
Save NitroXenon/6735f7079027b2758693 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
"""
Simple script that implements the minecraft protocol
to create a basic chat client for said game.
No encryption, no online mode, no parsing of chat messages.
I tried to make it as extendable as possible, so hack away.
PEP8 Note: Ignored E302 (2 newlines between functions)
"""
# Global imports
from socket import socket, AF_INET, SOCK_STREAM
from sys import stderr, exit
from threading import Thread
from struct import pack, unpack, unpack_from, calcsize
# Settings
username = "mid_kid"
host = "localhost"
port = 25565
debug = False # Print all processed packets
# Set up the socket
sock = socket(AF_INET, SOCK_STREAM)
try:
sock.connect((host, port))
except ConnectionRefusedError:
print("Server is not online", file=stderr)
exit(1)
# Data types
# https://gist.github.com/barneygale/1209061
def varint_pack(d):
o = b''
while True:
b = d & 0x7F
d >>= 7
o += pack("B", b | (0x80 if d > 0 else 0))
if d == 0:
break
return o
def varint_unpack(s):
d, l = 0, 0
length = len(s)
if length > 5:
length = 5
for i in range(length):
l += 1
b = s[i]
d |= (b & 0x7F) << 7 * i
if not b & 0x80:
break
return (d, s[l:])
# Lots of packets have a varint in front of a value, saying how long it is.
def data_pack(data):
return varint_pack(len(data)) + data
def data_unpack(bytes):
length, bytes = varint_unpack(bytes)
return bytes[:length], bytes[length:]
# Same as data_*, but encoding and decoding strings, because I'm lazy.
def string_pack(string):
return data_pack(string.encode())
def string_unpack(bytes):
string, rest = data_unpack(bytes)
return string.decode(), rest
# Same as struct.unpack_from, but returns remaining data.
def struct_unpack(format, struct):
data = unpack_from(format, struct)
rest = struct[calcsize(format):]
return data, rest
# Minecraft has a different set of packets depending on what it's doing.
# Only implemented handshake, login and play here, but status also exists.
mode = "handshake"
packets = {
"send": {
"handshake": {},
"login": {},
"play": {}
},
"receive": {
"login": {},
"play": {}
}
}
version = 5 # Minecraft 1.7.6 - 1.7.9
# Have I joined the game? (You can't chat before that)
joined = False
# Send packets
def send(packet, *args, **kwargs):
func = packets["send"][mode][packet]
packid = varint_pack(int(func.packid, 16))
data = func(*args, **kwargs)
sock.sendall(data_pack(packid + data))
# Receive packets
def receive(data=None):
if isinstance(data, type(None)):
data = sock.recv(1024)
if not data:
return
data = data_unpack(data)[0]
packid, data = varint_unpack(data)
packid = str(hex(packid))
packs = packets["receive"][mode]
if packid not in packs:
return
packet = packs[packid]
return packet.packname, packet(data)
# Packet decorator. This adds the functions for the packets to the dict.
def packet(direc, mode, packname, packid):
def decor(func):
if direc == "send":
func.packid = packid
packets[direc][mode][packname] = func
elif direc == "receive":
func.packname = packname
packets[direc][mode][packid] = func
return func
return decor
# The packets
# http://wiki.vg/Protocol#Disconnect
# http://wiki.vg/Protocol#Disconnect_2
@packet("receive", "play", "disconnect", "0x40")
@packet("receive", "login", "disconnect", "0x0")
def _p(data):
message = string_unpack(data)[0]
print("Disconnected from server: " + message)
return message
# http://wiki.vg/Protocol#Login_Success
@packet("receive", "login", "success", "0x2")
def _p(data):
global mode
mode = "play"
uuid, data = string_unpack(data)
name = string_unpack(data)[0]
return uuid, name
# http://wiki.vg/Protocol#Keep_Alive
@packet("receive", "play", "keep-alive", "0x0")
def _p(data):
id = unpack("!i", data)[0]
send("keep-alive", id)
return id
# http://wiki.vg/Protocol#Join_Game
@packet("receive", "play", "joined", "0x1")
def _p(data):
global joined
joined = True
stuff, data = struct_unpack('!iBbBB', data)
level_type = string_unpack(data)[0]
return stuff + (level_type, )
# http://wiki.vg/Protocol#Chat_Message
@packet("receive", "play", "chat", "0x2")
def _p(data):
message = string_unpack(data)[0]
# Insert parsing code here, I'm too lazy.
print(message)
return message
# http://wiki.vg/Protocol#Handshake
@packet("send", "handshake", "handshake", "0x0")
def _p(version, host, port, next):
if next == 2:
global mode
mode = "login"
version = varint_pack(version)
host = string_pack(host)
port = pack('!H', port)
next = varint_pack(next)
return version + host + port + next
# http://wiki.vg/Protocol#Login_Start
@packet("send", "login", "start", "0x0")
def _p(name):
name = string_pack(name)
return name
# http://wiki.vg/Protocol#Keep_Alive_2
@packet("send", "play", "keep-alive", "0x0")
def _p(id):
id = pack('!i', id)
return id
# http://wiki.vg/Protocol#Chat_Message_2
@packet("send", "play", "chat", "0x1")
def _p(message):
message = string_pack(message)
return message
stop = False
# Listen to incoming packets
def listen():
global stop
while not stop:
data = sock.recv(1024)
if not data:
print("Connection Lost.")
stop = True
msg = receive(data)
if debug and msg:
print(msg)
Thread(target=listen).start()
# Login
send("handshake", version, host, port, 2)
send("start", username)
# Send chat messages
try:
while not joined and not stop:
pass
while True:
text = input()
if stop:
break
send("chat", text)
except KeyboardInterrupt:
if not stop:
print("\nDisconnecting...")
stop = True
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment