Commits

Pierre-Marie de Rodat committed 2a2370a

Completed the list of handled packets and improved the test client.

Test client handles gravity and try to move randomly.

  • Participants
  • Parent commits 36b1002

Comments (0)

Files changed (4)

 # -*- coding: utf-8 -*-
 
+import collections
+import StringIO
+
 import gevent
 import gevent.queue
 import gevent.socket as socket
 
+import map
 from packet import *
 
 
         self.receiving_coroutine = None
         self.sending_coroutine = None
         self.sending_queue = gevent.queue.Queue(maxsize=5)
+        self.map = None
+        self.spawned = False
+        self.spawn_position = None
+        self.player_coroutine = None
+
+        # Initialize packet handlers: the default one does nothing.
+        self.handlers = collections.defaultdict(lambda: (lambda packet: None))
+        self.handlers.update({
+            KeepAlive.packet_id: self.handle_keepalive,
+            SpawnPosition.packet_id: self.handle_spawnposition,
+            PlayerPosition.packet_id: self.handle_playerposition,
+            PlayerPositionLookServer.packet_id: self.handle_playerpositionlook,
+            PreChunk.packet_id: self.handle_prechunk,
+            MapChunk.packet_id: self.handle_mapchunk,
+            ChatMessage.packet_id: self.handle_chat,
+        })
+        self.connected = False
 
     def connect(self, address):
         try:
         response = self.recv_packet()
         self.expect_packet(response, LoginRequestServer)
 
-        # Login is successful, start network coroutines.
+        # Login is successful, start network coroutines and initialize data.
+        self.connected = True
         self.receiving_coroutine = gevent.Greenlet.spawn(self.receive_forever)
         self.sending_coroutine = gevent.Greenlet.spawn(self.send_forever)
+        self.player_coroutine = gevent.Greenlet.spawn(self.serve_player)
+        self.map = map.Map()
+        self.player = map.Player(self.map)
 
     def expect_packet(self, packet, expected):
         if isinstance(packet, Disconnect):
     def receive_forever(self):
         while True:
             packet = self.recv_packet()
-
-            if isinstance(packet, KeepAlive):
-                # The server testst the connection sending a Keep Alive packet.
-                # We must send back the same packet.
-                self.put_packet(packet)
+            self.handlers[packet.packet_id](packet)
+            gevent.sleep()
 
     def send_packet(self, packet):
         try:
             packet = self.sending_queue.get()
             self.send_packet(packet)
 
+    def serve_player(self):
+        '''
+        Send to the server the player position every 50 ms once spawn.
+        '''
+        while self.connected:
+            if self.spawned:
+                self.put_packet(self.player.get_position_packet())
+            gevent.sleep(0.05)
+
     def on_disconnection(self, exception):
         '''
         Simple hook, can be overridden by subclasses. 'exception' contains the
         exception that caused the disconnection, or None if the disconnection
         was expected.
         '''
-        pass
+        self.connected = False
 
     def close(self):
         self.socket.close()
         self.on_disconnection(None)
+
+    def handle_keepalive(self, packet):
+        # The server testst the connection sending a Keep Alive packet.
+        # We must send back the same packet.
+        self.put_packet(packet)
+
+    def handle_chat(self, packet):
+        pass
+
+    def handle_spawnposition(self, packet):
+        print 'Got spawnposition'
+        self.spawn_position = (packet.x, packet.y, packet.z)
+        if not self.spawned:
+            self.player.set_position(packet.x, packet.y, packet.z)
+
+    def handle_playerposition(self, packet):
+        print 'Got playerposition: %s' % packet
+        self.player.set_position(packet.x, packet.y, packet.z, packet.stance)
+        self.spawned = True
+
+    def handle_playerpositionlook(self, packet):
+        print 'Got playerpositionlook: %s' % packet
+        self.player.set_position(packet.x, packet.y, packet.z, packet.stance)
+        self.player.set_look(packet.yaw, packet.pitch)
+        if not self.spawned:
+            self.spawned = True
+
+    def handle_prechunk(self, packet):
+        if packet.mode:
+            self.map.put((packet.x, packet.z), map.Chunk())
+        else:
+            self.map.remove((packet.x, packet.z))
+        if len(self.map.chunks) % 30 == 0:
+            print '-> %s' % len(self.map.chunks)
+
+    def handle_mapchunk(self, packet):
+        chunk_x = packet.x >> 4
+        chunk_y = packet.y >> 7
+        chunk_z = packet.z >> 4
+        chunk = self.map.get((chunk_x, chunk_z))
+        start_x = packet.x & 15
+        start_y = packet.y & 127
+        start_z = packet.z & 15
+        count = (packet.size_x + 1) * (packet.size_y + 1) * (packet.size_z + 1)
+
+        data = StringIO.StringIO(packet.data)
+        byte_format = '>' + 'B' * count
+        half_format = '>' + 'B' * (count / 2)
+        block_types = list(struct.unpack(
+            byte_format, data.read(count)
+        ))
+        metadata = list(struct.unpack(
+            half_format, data.read(count / 2)
+        ))
+        light = list(struct.unpack(
+            half_format, data.read(count / 2)
+        ))
+        sky_light = list(struct.unpack(
+            half_format, data.read(count / 2)
+        ))
+
+        def get_half(array, is_first_half):
+            return (
+                array[0] & 0xF
+                if is_first_half else
+                array.pop(0) >> 4
+            )
+
+        first_half = True
+        for i in xrange(count):
+            chunk.put(
+                (
+                    start_x + i >> 11,
+                    start_y + i & 0x7f,
+                    start_z + (i & 0x780) >> 7
+                ),
+                map.Block(
+                    block_types.pop(0),
+                    get_half(metadata, first_half),
+                    get_half(light, first_half),
+                    get_half(sky_light, first_half),
+                )
+            )
+            first_half = not first_half
+# -*- coding: utf-8 -*-
+
+import collections
+
+from packet import PlayerPositionLookClient
+
+
+
+Block = collections.namedtuple(
+    'Block',
+    ('type', 'metadata', 'light', 'sky_light')
+)
+
+empty_block = Block(0, 0, 0, 0)
+
+class Chunk(object):
+
+    WIDTH = 16
+    HEIGHT = 128
+    DEPTH = 16
+
+    def __init__(self):
+        self.blocks = [
+            empty_block
+            for i in xrange(self.WIDTH * self.HEIGHT * self.DEPTH)
+        ]
+
+    def get(self, coordinates):
+        x, y, z = coordinates
+        return self.blocks[
+            y + (z * self.HEIGHT) + (x * self.HEIGHT * self.DEPTH)
+        ]
+
+    def put(self, coordinates, block):
+        x, y, z = coordinates
+        self.blocks[
+            y + (z * self.HEIGHT) + (x * self.HEIGHT * self.DEPTH)
+        ] = block
+
+
+
+class Map(object):
+    def __init__(self):
+        self.chunks = {}
+
+    def get(self, coordinates):
+        try:
+            return self.chunks[coordinates]
+        except KeyError:
+            return None
+
+    def put(self, coordinates, chunk):
+        self.chunks[coordinates] = chunk
+        chunk.coordinates = coordinates
+
+    def remove(self, coordinates):
+        # If the server tells to remove a not allocated chunk, something *is*
+        # wrong, hence we must raise an error.
+        self.chunks.pop(coordinates)
+
+class Player(object):
+    def __init__(self, map):
+        self.map = map
+        self.x = 0
+        self.y = 0
+        self.z = 0
+        self.stance = 0.11
+        self.yaw = 0
+        self.pitch = 0
+        self.on_ground = False
+
+    def set_position(self, x=None, y=None, z=None, stance=None):
+        if x is not None:
+            self.x = x
+        if y is not None:
+            self.y = y
+        if z is not None:
+            self.z = z
+        if stance is not None:
+            self.stance = stance
+        else:
+            self.stance = self.y + 0.5
+
+    def set_look(self, yaw, pitch):
+        self.yaw, self.pitch = yaw, pitch
+
+    def get_chunk(self):
+        return self.map.get((int(self.x) >> 4, int(self.z) >> 4))
+
+    def get_position_packet(self):
+        return PlayerPositionLookClient(
+            x=self.x, y=self.y, z=self.z,
+            stance=self.stance, on_ground=self.on_ground,
+            yaw=self.yaw, pitch=self.pitch
+        )
 import logging
 import socket
 import struct
+import zlib
 
 
 
+INVALID_BED = 0
+BEGIN_RAINING = 1
+END_RAINING = 2
+CHANGE_GAME_MODE = 3
+ENTER_CREDITS = 4
+
 NO_ANIMATION=0
 SWING_ARM=1
 DAMAGE_ANIMATION=2
 CROUCH=104
 UNCROUCH=105
 
+ACTION_CROUCH = 1
+ACTION_UNCROUCH = 2
+ACTION_LEAVE_BED = 3
+ACTION_START_SPRINTING = 4
+ACTION_STOP_SPRINTING = 5
+
 ENTITY_HURT=2
 ENTITY_DEAD=3
 WOLF_TAMING=6
     @classmethod
     def parse(cls, socket):
         data_size = Int.parse(socket)
-        # TODO: extract chunk data
-        return recvall(socket, data_size)
+        return zlib.decompress(recvall(socket, data_size))
 
 
 class Packet(object):
                     logging.warning(
                         'Overriding %s (%s) with %s',
                         old_class.__name__,
-                        packet_id,
+                        hex(packet_id),
                         packet_class.__name__
                     )
                 register[packet_id] = packet_class
         ('z', Int),
     )
 
+@Packet.registered(Packet.CLIENT, 0x07)
+class UseEntity(Packet):
+    fields = (
+        ('user', Int),
+        ('target', Int),
+        ('left_click', Bool),
+    )
+
 @Packet.registered(Packet.SERVER, 0x08)
 class UpdateHealth(Packet):
     fields = (
         ('on_ground', Bool),
     )
 
+@Packet.registered(Packet.BOTH, 0x0E)
+class PlayerDigging(Packet):
+    fields = (
+        ('status', Byte),
+        ('x', Int),
+        ('y', Byte),
+        ('z', Int),
+        ('face', Byte),
+    )
+
+@Packet.registered(Packet.CLIENT, 0x0F)
+class PlayerBlockPlacement(Packet):
+    fields = (
+        ('x', Int),
+        ('y', Byte),
+        ('z', Int),
+        ('direction', Byte),
+        ('held_item', Slot),
+    )
+
+@Packet.registered(Packet.CLIENT, 0x10)
+class PlayerBlockPlacement(Packet):
+    fields = (
+        ('slot_id', Short),
+    )
+
+@Packet.registered(Packet.SERVER, 0x11)
+class PlayerBlockPlacement(Packet):
+    fields = (
+        ('entity_id', Int),
+        ('in_bed', Byte),
+        ('head_x', Int),
+        ('head_y', Byte),
+        ('head_z', Int),
+    )
+
 @Packet.registered(Packet.BOTH, 0x12)
 class Animation(Packet):
     fields = (
         ('animation', Byte),
     )
 
+@Packet.registered(Packet.CLIENT, 0x13)
+class EntityAction(Packet):
+    fields = (
+        ('entity_id', Int),
+        ('action_id', Byte),
+    )
+
 @Packet.registered(Packet.SERVER, 0x14)
 class NamedEntitySpawn(Packet):
     fields = (
         ('roll', Byte),
     )
 
+@Packet.registered(Packet.SERVER, 0x16)
+class CollectedItem(Packet):
+    fields = (
+        ('entity_id', Int),
+        ('collected_id', Int),
+    )
+
+@Packet.registered(Packet.SERVER, 0x17)
+class AddObjectVehicle(Packet):
+    fields = (
+        ('entity_id', Int),
+        ('type', Byte),
+        ('x', Int),
+        ('y', Int),
+        ('z', Int),
+        ('thrower_id', Int),
+        ('speed_x', Short),
+        ('speed_y', Short),
+        ('speed_z', Short),
+    )
+
 @Packet.registered(Packet.SERVER, 0x18)
 class MobSpawn(Packet):
     fields = (
         ('metadata', Metadata),
     )
 
+@Packet.registered(Packet.SERVER, 0x19)
+class EntityPanting(Packet):
+    fields = (
+        ('entity_id', Int),
+        ('title', String),
+        ('x', Int),
+        ('y', Int),
+        ('z', Int),
+        ('direction', Int),
+    )
+
+@Packet.registered(Packet.SERVER, 0x1a)
+class ExperienceOrb(Packet):
+    fields = (
+        ('entity_id', Int),
+        ('x', Int),
+        ('y', Int),
+        ('z', Int),
+        ('count', Short),
+    )
+
 @Packet.registered(Packet.BOTH, 0x1C)
 class EntityVelocity(Packet):
     fields = (
         ('entity_status', Byte),
     )
 
+@Packet.registered(Packet.BOTH, 0x27)
+class AttachEntity(Packet):
+    fields = (
+        ('entity_id', Int),
+        ('vehicle_id', Int),
+    )
+
+@Packet.registered(Packet.SERVER, 0x28)
+class EntityMetadata(Packet):
+    fields = (
+        ('entity_id', Int),
+        ('metadata', Metadata),
+    )
+
+@Packet.registered(Packet.SERVER, 0x29)
+class EntityEffect(Packet):
+    fields = (
+        ('entity_id', Int),
+        ('effect_id', Byte),
+        ('amplifier', Byte),
+        ('duration', Short),
+    )
+
+@Packet.registered(Packet.BOTH, 0x2A)
+class AttachEntity(Packet):
+    fields = (
+        ('entity_id', Int),
+        ('effect_id', Byte),
+    )
+
+@Packet.registered(Packet.SERVER, 0x2B)
+class Experience(Packet):
+    fields = (
+        ('experience_bar', Float),
+        ('level', Short),
+        ('total_experience', Short),
+    )
+
 @Packet.registered(Packet.SERVER, 0x32)
 class PreChunk(Packet):
     fields = (
     fields = (
         ('x', Int),
         ('y', Short),
-        ('w', Int),
+        ('z', Int),
         ('size_x', Byte),
         ('size_y', Byte),
         ('size_z', Byte),
         ('byte2', Byte),
     )
 
+@Packet.registered(Packet.SERVER, 0x3C)
+class Explosion(Packet):
+    fields = (
+        ('x', Double),
+        ('y', Double),
+        ('z', Double),
+        ('_unknown', Float),
+        ('records', MultiArray(
+            Int, [
+                ('x', Byte),
+                ('y', Byte),
+                ('z', Byte),
+            ]
+        ))
+    )
+
+@Packet.registered(Packet.SERVER, 0x3D)
+class SoundEffect(Packet):
+    fields = (
+        ('effect_id', Int),
+        ('x', Int),
+        ('y', Byte),
+        ('z', Int),
+        ('data', Int),
+    )
+
+@Packet.registered(Packet.SERVER, 0x46)
+class NewOrInvalidState(Packet):
+    fields = (
+        ('reason', Byte),
+        ('game_mode', Byte),
+    )
+
+@Packet.registered(Packet.SERVER, 0x47)
+class Thunderbolt(Packet):
+    fields = (
+        ('entity_id', Int),
+        ('_unknown', Bool),
+        ('x', Int),
+        ('y', Int),
+        ('z', Int)
+    )
+
+@Packet.registered(Packet.SERVER, 0x64)
+class OpenWindow(Packet):
+    fields = (
+        ('window_id', Byte),
+        ('inventory_type', Byte),
+        ('window_title', String),
+        ('slots_count', Byte),
+    )
+
+@Packet.registered(Packet.SERVER, 0x65)
+class CloseWindow(Packet):
+    fields = (
+        ('window_id', Byte),
+    )
+
+@Packet.registered(Packet.CLIENT, 0x66)
+class WindowClick(Packet):
+    fields = (
+        ('window_id', Byte),
+        ('slot', Short),
+        ('right_click', Byte),
+        ('action_number', Short),
+        ('shift', Bool),
+        ('clicked_item', Slot),
+    )
+
 @Packet.registered(Packet.SERVER, 0x67)
 class SetSlot(Packet):
     fields = (
         ('slots', Array(Short, Slot)),
     )
 
+@Packet.registered(Packet.SERVER, 0x69)
+class UpdateWindowProperty(Packet):
+    fields = (
+        ('window_id', Byte),
+        ('property', Short),
+        ('value', Short),
+    )
+
+@Packet.registered(Packet.SERVER, 0x6B)
+class Transaction(Packet):
+    fields = (
+        ('window_id', Byte),
+        ('action_number', Short),
+        ('accepted', Bool),
+    )
+
+@Packet.registered(Packet.SERVER, 0x6B)
+class CreateInventoryAction(Packet):
+    fields = (
+        ('slot', Short),
+        ('clicked_item', Slot),
+    )
+
+@Packet.registered(Packet.CLIENT, 0x6C)
+class EnchantItem(Packet):
+    fields = (
+        ('window_id', Byte),
+        ('enchantment', Byte),
+    )
+
+@Packet.registered(Packet.SERVER, 0x82)
+class UpdateSign(Packet):
+    fields = (
+        ('x', Int),
+        ('y', Short),
+        ('z', Int),
+        ('line1', String),
+        ('line2', String),
+        ('line3', String),
+        ('line4', String),
+    )
+
+@Packet.registered(Packet.SERVER, 0x83)
+class ItemData(Packet):
+    fields = (
+        ('item_type', Short),
+        ('item_id', Short),
+        ('text', Array(UnsignedByte, Byte)),
+    )
+
+@Packet.registered(Packet.SERVER, 0xC8)
+class IncrementStatistic(Packet):
+    fields = (
+        ('statistic_id', Int),
+        ('amount', Byte),
+    )
+
 @Packet.registered(Packet.SERVER, 0xC9)
 class PlayerListItem(Packet):
     fields = (
         ('ping', Short),
     )
 
+@Packet.registered(Packet.SERVER, 0xFA)
+class PluginMessage(Packet):
+    fields = (
+        ('channel', String),
+        ('data', Array(Short, Byte)),
+    )
+
+@Packet.registered(Packet.CLIENT, 0xFE)
+class ServerListPing(Packet):
+    fields = ()
+
 @Packet.registered(Packet.BOTH, 0xFF)
 class Disconnect(Packet):
     fields = (

File test_real.py

 # -*- coding: utf-8 -*-
 
 import logging
+import math
+import random
 import sys
 
 import gevent
+import gevent.event
 
 from client import Client
 import packet
 main_logger.setLevel(logging.DEBUG)
 logging.root = main_logger
 
-username, host, port = sys.argv[1:4]
-c = Client(username)
-c.connect((host, int(port)))
-gevent.sleep(1)
-c.put_packet(packet.Disconnect(reason='Goodbye!'))
-gevent.sleep(1)
-c.close()
+
+
+class MyClient(Client):
+    def __init__(self, username):
+        super(MyClient, self).__init__(username)
+        self.is_ready = gevent.event.Event()
+
+    def connect(self, address):
+        super(MyClient, self).connect(address)
+
+    def handle_prechunk(self, packet):
+        super(MyClient, self).handle_prechunk(packet)
+
+    def handle_mapchunk(self, packet):
+        super(MyClient, self).handle_mapchunk(packet)
+        if self.player.get_chunk() is not None:
+            self.is_ready.set()
+
+    def handle_spawnposition(self, packet):
+        super(MyClient, self).handle_spawnposition(packet)
+
+    def handle_chat(self, packet):
+        pass
+
+    def handle_gravity(self):
+        p = self.player
+        chunk = p.get_chunk()
+        x, y, z = (
+            int(p.x) & 0xF, int(p.y), int(p.z) & 0xF
+        )
+        print 'Before:', p.y
+        bottom = chunk.get((x, y, z))
+        under = chunk.get((x, y - 1, z))
+        stable = False
+        if bottom.type == 0 and under.type == 0:
+            print 'Im falling'
+            p.set_position(y=p.y - 0.3)
+            p.on_ground = False
+        elif bottom.type != 0:
+            print 'Im in solid, going up'
+            p.set_position(y=p.y + 0.1)
+        elif under.type != 0:
+            print 'Im on ground'
+            p.on_groud = True
+            stable = True
+        print 'After:', p.y
+        self.put_packet(p.get_position_packet())
+        return stable
+
+
+
+def main():
+    username, host, port = sys.argv[1:4]
+    c = MyClient(username)
+    try:
+        c.connect((host, int(port)))
+        c.is_ready.wait()
+        speed = 0.3
+        dx, dz = 0, speed
+        p = c.player
+        while True:
+            stable = c.handle_gravity()
+            nx, nz = p.x + dx, p.z + dz
+            chunk = c.map.get((int(nx) >> 4, int(nz) >> 4))
+            can_walk = False
+            if stable and chunk is not None:
+                print 'my chunk is not None'
+                x, y, z = (
+                    int(nx) & 0xF,
+                    int(p.y),
+                    int(nz) & 0xF
+                )
+                top, bottom, under = (
+                    chunk.get((x, y + 1, z)),
+                    chunk.get((x, y, z)),
+                    chunk.get((x, y - 1, z)),
+                )
+                can_walk = (
+                    top.type == 0 and bottom.type == 0 and under.type != 0
+                )
+            else:
+                print 'my chunk is None'
+            if can_walk:
+                # The way is free: walk
+                print 'I can walk!'
+                p.set_position(x=nx, z=nz)
+                c.put_packet(p.get_position_packet())
+            elif stable:
+                print '\x1b[31mChanging direction\x1b[0m'
+                p.set_position(x=nx, z=nz)
+                direction = random.random() * math.pi
+                dx = math.sin(direction) * speed
+                dz = math.cos(direction) * speed
+            gevent.sleep(.1)
+        c.put_packet(packet.Disconnect(reason='Goodbye!'))
+        gevent.sleep(1)
+        c.close()
+    except KeyboardInterrupt:
+        c.close()
+
+if __name__ == '__main__':
+    main()