예제 #1
0
class TestLocation(unittest.TestCase):

    def setUp(self):
        self.l = Location()

    def test_trivial(self):
        pass

    def test_str(self):
        str(self.l)

    def test_clamp_stance(self):
        """
        Clamped stance should be 1.62 blocks above the current block.
        """

        self.l.pos = Position(0, 32, 0)
        self.l.clamp()
        self.assertAlmostEqual(self.l.stance, 2.62)

    def test_save_to_packet(self):
        self.assertTrue(self.l.save_to_packet())

    def test_in_front_of(self):
        other = self.l.in_front_of(1)

        self.assertEqual(other.pos.x, 0)
        self.assertEqual(other.pos.z, 32)

    def test_in_front_of_yaw(self):
        self.l.ori = Orientation.from_degs(90, 0)
        other = self.l.in_front_of(1)

        self.assertEqual(other.pos.x, -32)
        self.assertEqual(other.pos.z, 0)
예제 #2
0
class TestLocation(unittest.TestCase):

    def setUp(self):
        self.l = Location()

    def test_trivial(self):
        pass

    def test_str(self):
        str(self.l)

    def test_save_to_packet(self):
        self.assertTrue(self.l.save_to_packet())

    def test_in_front_of(self):
        other = self.l.in_front_of(1)

        self.assertEqual(other.pos.x, 0)
        self.assertEqual(other.pos.z, 32)

    def test_in_front_of_yaw(self):
        self.l.ori = Orientation.from_degs(90, 0)
        other = self.l.in_front_of(1)

        self.assertEqual(other.pos.x, -32)
        self.assertEqual(other.pos.z, 0)
예제 #3
0
    def OnPlayerLocationUpdate(self, packet):
        self.bot.update_location_from_packet(packet)

        print packet

        # everytime the player spawns, it must send back the location that it was given
        # this is a check for the server.  not entirely part of authentication, but
        # the bot won't run without it
        if self.confirmed_spawn == False:
            location = Location()
            location.load_from_packet(packet)
            p = location.save_to_packet()
            self.transport.write(p)
            self.confirmed_spawn = True
            self.bot.set_location(location)
            self.bot.OnReady()
예제 #4
0
class TestLocation(unittest.TestCase):
    def setUp(self):
        self.l = Location()

    def test_trivial(self):
        pass

    def test_default_stance(self):
        self.assertEqual(self.l.stance, 1.0)

    def test_save_to_packet(self):
        self.assertTrue(self.l.save_to_packet())

    def test_distance(self):
        other = Location()
        other.x = 2
        other.y = 3
        other.z = 6
        self.assertEqual(self.l.distance(other), 7)
예제 #5
0
class TestLocation(unittest.TestCase):
    def setUp(self):
        self.l = Location()

    def test_trivial(self):
        pass

    def test_str(self):
        str(self.l)

    def test_clamp_stance(self):
        """
        Clamped stance should be 1.62 blocks above the current block.
        """

        self.l.pos = Position(0, 32, 0)
        self.l.clamp()
        self.assertAlmostEqual(self.l.stance, 2.62)

    def test_clamp_void(self):
        """
        Locations in the Void should be clamped to above bedrock.
        """

        self.l.pos = Position(0, -32, 0)
        self.assertTrue(self.l.clamp())
        self.assertEqual(self.l.pos.y, 32)

    def test_save_to_packet(self):
        self.assertTrue(self.l.save_to_packet())

    def test_in_front_of(self):
        other = self.l.in_front_of(1)

        self.assertEqual(other.pos.x, 0)
        self.assertEqual(other.pos.z, 32)

    def test_in_front_of_yaw(self):
        self.l.ori = Orientation.from_degs(90, 0)
        other = self.l.in_front_of(1)

        self.assertEqual(other.pos.x, -32)
        self.assertEqual(other.pos.z, 0)
예제 #6
0
class BetaServerProtocol(object, Protocol, TimeoutMixin):
    """
    The Minecraft Alpha/Beta server protocol.

    This class is mostly designed to be a skeleton for featureful clients. It
    tries hard to not step on the toes of potential subclasses.
    """

    excess = ""
    packet = None

    state = STATE_UNAUTHENTICATED

    buf = ""
    parser = None
    handler = None

    player = None
    username = None
    settings = Settings("en_US", "normal")
    motd = "Bravo Generic Beta Server"

    _health = 20
    _latency = 0

    def __init__(self):
        self.chunks = dict()
        self.windows = []
        self.wid = 1

        self.location = Location()

        self.handlers = {
            0: self.ping,
            1: self.login,
            2: self.handshake,
            3: self.chat,
            7: self.use,
            9: self.respawn,
            10: self.grounded,
            11: self.position,
            12: self.orientation,
            13: self.location_packet,
            14: self.digging,
            15: self.build,
            16: self.equip,
            18: self.animate,
            19: self.action,
            21: self.pickup,
            101: self.wclose,
            102: self.waction,
            106: self.wacknowledge,
            107: self.wcreative,
            130: self.sign,
            204: self.settings_packet,
            254: self.poll,
            255: self.quit,
        }

        self._ping_loop = LoopingCall(self.update_ping)

        self.setTimeout(30)

    # Low-level packet handlers
    # Try not to hook these if possible, since they offer no convenient
    # abstractions or protections.

    def ping(self, container):
        """
        Hook for ping packets.

        By default, this hook will examine the timestamps on incoming pings,
        and use them to estimate the current latency of the connected client.
        """

        now = timestamp_from_clock(reactor)
        then = container.pid

        self.latency = now - then

    def login(self, container):
        """
        Hook for login packets.

        Override this to customize how logins are handled. By default, this
        method will only confirm that the negotiated wire protocol is the
        correct version, and then it will run the ``authenticated()``
        callback.
        """

        if container.protocol < SUPPORTED_PROTOCOL:
            # Kick old clients.
            self.error("This server doesn't support your ancient client.")
        elif container.protocol > SUPPORTED_PROTOCOL:
            # Kick new clients.
            self.error("This server doesn't support your newfangled client.")
        else:
            reactor.callLater(0, self.authenticated)

    def handshake(self, container):
        """
        Hook for handshake packets.
        """

    def chat(self, container):
        """
        Hook for chat packets.
        """

    def use(self, container):
        """
        Hook for use packets.
        """

    def respawn(self, container):
        """
        Hook for respawn packets.
        """

    def grounded(self, container):
        """
        Hook for grounded packets.
        """

        self.location.grounded = bool(container.grounded)

    def position(self, container):
        """
        Hook for position packets.
        """

        if self.state != STATE_LOCATED:
            log.msg("Ignoring unlocated position!")
            return

        self.grounded(container.grounded)

        old_position = self.location.pos
        position = Position.from_player(container.position.x,
                container.position.y, container.position.z)
        altered = False

        dx, dy, dz = old_position - position
        if any(abs(d) >= 64 for d in (dx, dy, dz)):
            # Whoa, slow down there, cowboy. You're moving too fast. We're
            # gonna ignore this position change completely, because it's
            # either bogus or ignoring a recent teleport.
            altered = True
        else:
            self.location.pos = position
            self.location.stance = container.position.stance

        # Santitize location. This handles safety boundaries, illegal stance,
        # etc.
        altered = self.location.clamp() or altered

        # If, for any reason, our opinion on where the client should be
        # located is different than theirs, force them to conform to our point
        # of view.
        if altered:
            log.msg("Not updating bogus position!")
            self.update_location()

        # If our position actually changed, fire the position change hook.
        if old_position != position:
            self.position_changed()

    def orientation(self, container):
        """
        Hook for orientation packets.
        """

        self.grounded(container.grounded)

        old_orientation = self.location.ori
        orientation = Orientation.from_degs(container.orientation.rotation,
                container.orientation.pitch)
        self.location.ori = orientation

        if old_orientation != orientation:
            self.orientation_changed()

    def location_packet(self, container):
        """
        Hook for location packets.
        """

        self.position(container)
        self.orientation(container)

    def digging(self, container):
        """
        Hook for digging packets.
        """

    def build(self, container):
        """
        Hook for build packets.
        """

    def equip(self, container):
        """
        Hook for equip packets.
        """

    def pickup(self, container):
        """
        Hook for pickup packets.
        """

    def animate(self, container):
        """
        Hook for animate packets.
        """

    def action(self, container):
        """
        Hook for action packets.
        """

    def wclose(self, container):
        """
        Hook for wclose packets.
        """

    def waction(self, container):
        """
        Hook for waction packets.
        """

    def wacknowledge(self, container):
        """
        Hook for wacknowledge packets.
        """

    def wcreative(self, container):
        """
        Hook for creative inventory action packets.
        """

    def sign(self, container):
        """
        Hook for sign packets.
        """

    def settings_packet(self, container):
        """
        Hook for client settings packets.
        """

        distance = ["far", "normal", "short", "tiny"][container.distance]
        self.settings = Settings(container.locale, distance)

    def poll(self, container):
        """
        Hook for poll packets.

        By default, queries the parent factory for some data, and replays it
        in a specific format to the requester. The connection is then closed
        at both ends. This functionality is used by Beta 1.8 clients to poll
        servers for status.
        """

        players = unicode(len(self.factory.protocols))
        max_players = unicode(self.factory.limitConnections or 1000000)

        data = [
            u"§1",
            unicode(SUPPORTED_PROTOCOL),
            u"Bravo %s" % version,
            self.motd,
            players,
            max_players,
        ]

        response = u"\u0000".join(data)
        self.error(response)

    def quit(self, container):
        """
        Hook for quit packets.

        By default, merely logs the quit message and drops the connection.

        Even if the connection is not dropped, it will be lost anyway since
        the client will close the connection. It's better to explicitly let it
        go here than to have zombie protocols.
        """

        log.msg("Client is quitting: %s" % container.message)
        self.transport.loseConnection()

    # Twisted-level data handlers and methods
    # Please don't override these needlessly, as they are pretty solid and
    # shouldn't need to be touched.

    def dataReceived(self, data):
        self.buf += data

        packets, self.buf = parse_packets(self.buf)

        if packets:
            self.resetTimeout()

        for header, payload in packets:
            if header in self.handlers:
                self.handlers[header](payload)
            else:
                log.err("Didn't handle parseable packet %d!" % header)
                log.err(payload)

    def connectionLost(self, reason):
        if self._ping_loop.running:
            self._ping_loop.stop()

    def timeoutConnection(self):
        self.error("Connection timed out")

    # State-change callbacks
    # Feel free to override these, but call them at some point.

    def challenged(self):
        """
        Called when the client has started authentication with the server.
        """

        self.state = STATE_CHALLENGED

    def authenticated(self):
        """
        Called when the client has successfully authenticated with the server.
        """

        self.state = STATE_AUTHENTICATED

        self._ping_loop.start(30)

    # Event callbacks
    # These are meant to be overriden.

    def orientation_changed(self):
        """
        Called when the client moves.

        This callback is only for orientation, not position.
        """

        pass

    def position_changed(self):
        """
        Called when the client moves.

        This callback is only for position, not orientation.
        """

        pass

    # Convenience methods for consolidating code and expressing intent. I
    # hear that these are occasionally useful. If a method in this section can
    # be used, then *PLEASE* use it; not using it is the same as open-coding
    # whatever you're doing, and only hurts in the long run.

    def write_packet(self, header, **payload):
        """
        Send a packet to the client.
        """

        self.transport.write(make_packet(header, **payload))

    def update_ping(self):
        """
        Send a keepalive to the client.
        """

        timestamp = timestamp_from_clock(reactor)
        self.write_packet("ping", pid=timestamp)

    def update_location(self):
        """
        Send this client's location to the client.

        Also let other clients know where this client is.
        """

        # Don't bother trying to update things if the position's not yet
        # synchronized. We could end up jettisoning them into the void.
        if self.state != STATE_LOCATED:
            return

        x, y, z = self.location.pos
        yaw, pitch = self.location.ori.to_fracs()

        # Inform everybody of our new location.
        packet = make_packet("teleport", eid=self.player.eid, x=x, y=y, z=z,
                yaw=yaw, pitch=pitch)
        self.factory.broadcast_for_others(packet, self)

        # Inform ourselves of our new location.
        packet = self.location.save_to_packet()
        self.transport.write(packet)

    def ascend(self, count):
        """
        Ascend to the next XZ-plane.

        ``count`` is the number of ascensions to perform, and may be zero in
        order to force this player to not be standing inside a block.

        :returns: bool of whether the ascension was successful

        This client must be located for this method to have any effect.
        """

        if self.state != STATE_LOCATED:
            return False

        x, y, z = self.location.pos.to_block()

        bigx, smallx, bigz, smallz = split_coords(x, z)

        chunk = self.chunks[bigx, bigz]
        column = [chunk.get_block((smallx, i, smallz)) for i in range(256)]

        # Special case: Ascend at most once, if the current spot isn't good.
        if count == 0:
            if not column[y] or column[y + 1] or column[y + 2]:
                # Yeah, we're gonna need to move.
                count += 1
            else:
                # Nope, we're fine where we are.
                return True

        for i in xrange(y, 126):
            # Find the next spot above us which has a platform and two empty
            # blocks of air.
            if column[i] and not column[i + 1] and not column[i + 2]:
                count -= 1
                if not count:
                    break
        else:
            return False

        self.location.pos = self.location.pos._replace(y=i * 32)
        return True

    def error(self, message):
        """
        Error out.

        This method sends ``message`` to the client as a descriptive error
        message, then closes the connection.
        """

        self.transport.write(make_error_packet(message))
        self.transport.loseConnection()

    def play_notes(self, notes):
        """
        Play some music.

        Send a sequence of notes to the player. ``notes`` is a finite iterable
        of pairs of instruments and pitches.

        There is no way to time notes; if staggered playback is desired (and
        it usually is!), then ``play_notes()`` should be called repeatedly at
        the appropriate times.

        This method turns the block beneath the player into a note block,
        plays the requested notes through it, then turns it back into the
        original block, all without actually modifying the chunk.
        """

        x, y, z = self.location.pos.to_block()

        if y:
            y -= 1

        bigx, smallx, bigz, smallz = split_coords(x, z)

        if (bigx, bigz) not in self.chunks:
            return

        block = self.chunks[bigx, bigz].get_block((smallx, y, smallz))
        meta = self.chunks[bigx, bigz].get_metadata((smallx, y, smallz))

        self.write_packet("block", x=x, y=y, z=z,
                          type=blocks["note-block"].slot, meta=0)

        for instrument, pitch in notes:
            self.write_packet("note", x=x, y=y, z=z, pitch=pitch,
                    instrument=instrument)

        self.write_packet("block", x=x, y=y, z=z, type=block, meta=meta)

    # Automatic properties. Assigning to them causes the client to be notified
    # of changes.

    @property
    def health(self):
        return self._health

    @health.setter
    def health(self, value):
        if not 0 <= value <= 20:
            raise BetaClientError("Invalid health value %d" % value)

        if self._health != value:
            self.write_packet("health", hp=value, fp=0, saturation=0)
            self._health = value

    @property
    def latency(self):
        return self._latency

    @latency.setter
    def latency(self, value):
        # Clamp the value to not exceed the boundaries of the packet. This is
        # necessary even though, in theory, a ping this high is bad news.
        value = clamp(value, 0, 65535)

        # Check to see if this is a new value, and if so, alert everybody.
        if self._latency != value:
            packet = make_packet("players", name=self.username, online=True,
                ping=value)
            self.factory.broadcast(packet)
            self._latency = value
예제 #7
0
class BetaServerProtocol(object, Protocol, TimeoutMixin):
    """
    The Minecraft Alpha/Beta server protocol.

    This class is mostly designed to be a skeleton for featureful clients. It
    tries hard to not step on the toes of potential subclasses.
    """

    excess = ""
    packet = None

    state = STATE_UNAUTHENTICATED

    buf = ""
    parser = None
    handler = None

    player = None
    username = None
    motd = "Bravo Generic Beta Server"

    _health = 20
    _latency = 0

    def __init__(self):
        self.chunks = dict()
        self.windows = []
        self.wid = 1

        self.location = Location()

        self.handlers = {
            0: self.ping,
            1: self.login,
            2: self.handshake,
            3: self.chat,
            7: self.use,
            9: self.respawn,
            10: self.grounded,
            11: self.position,
            12: self.orientation,
            13: self.location_packet,
            14: self.digging,
            15: self.build,
            16: self.equip,
            18: self.animate,
            19: self.action,
            21: self.pickup,
            101: self.wclose,
            102: self.waction,
            106: self.wacknowledge,
            107: self.wcreative,
            130: self.sign,
            254: self.poll,
            255: self.quit,
        }

        self._ping_loop = LoopingCall(self.update_ping)

        self.setTimeout(30)

    # Low-level packet handlers
    # Try not to hook these if possible, since they offer no convenient
    # abstractions or protections.

    def ping(self, container):
        """
        Hook for ping packets.

        By default, this hook will examine the timestamps on incoming pings,
        and use them to estimate the current latency of the connected client.
        """

        now = timestamp_from_clock(reactor)
        then = container.pid

        self.latency = now - then

    def login(self, container):
        """
        Hook for login packets.

        Override this to customize how logins are handled. By default, this
        method will only confirm that the negotiated wire protocol is the
        correct version, and then it will run the ``authenticated()``
        callback.
        """

        if container.protocol < SUPPORTED_PROTOCOL:
            # Kick old clients.
            self.error("This server doesn't support your ancient client.")
        elif container.protocol > SUPPORTED_PROTOCOL:
            # Kick new clients.
            self.error("This server doesn't support your newfangled client.")
        else:
            reactor.callLater(0, self.authenticated)

    def handshake(self, container):
        """
        Hook for handshake packets.
        """

    def chat(self, container):
        """
        Hook for chat packets.
        """

    def use(self, container):
        """
        Hook for use packets.
        """

    def respawn(self, container):
        """
        Hook for respawn packets.
        """

    def grounded(self, container):
        """
        Hook for grounded packets.
        """

        self.location.grounded = bool(container.grounded)

    def position(self, container):
        """
        Hook for position packets.
        """

        if self.state != STATE_LOCATED:
            log.msg("Ignoring unlocated position!")
            return

        self.grounded(container.grounded)

        old_position = self.location.pos
        position = Position.from_player(container.position.x,
                                        container.position.y,
                                        container.position.z)
        altered = False

        dx, dy, dz = old_position - position
        if any(abs(d) >= 64 for d in (dx, dy, dz)):
            # Whoa, slow down there, cowboy. You're moving too fast. We're
            # gonna ignore this position change completely, because it's
            # either bogus or ignoring a recent teleport.
            altered = True
        else:
            self.location.pos = position
            self.location.stance = container.position.stance

        # Santitize location. This handles safety boundaries, illegal stance,
        # etc.
        altered = self.location.clamp() or altered

        # If, for any reason, our opinion on where the client should be
        # located is different than theirs, force them to conform to our point
        # of view.
        if altered:
            log.msg("Not updating bogus position!")
            self.update_location()

        # If our position actually changed, fire the position change hook.
        if old_position != position:
            self.position_changed()

    def orientation(self, container):
        """
        Hook for orientation packets.
        """

        self.grounded(container.grounded)

        old_orientation = self.location.ori
        orientation = Orientation.from_degs(container.orientation.rotation,
                                            container.orientation.pitch)
        self.location.ori = orientation

        if old_orientation != orientation:
            self.orientation_changed()

    def location_packet(self, container):
        """
        Hook for location packets.
        """

        self.position(container)
        self.orientation(container)

    def digging(self, container):
        """
        Hook for digging packets.
        """

    def build(self, container):
        """
        Hook for build packets.
        """

    def equip(self, container):
        """
        Hook for equip packets.
        """

    def pickup(self, container):
        """
        Hook for pickup packets.
        """

    def animate(self, container):
        """
        Hook for animate packets.
        """

    def action(self, container):
        """
        Hook for action packets.
        """

    def wclose(self, container):
        """
        Hook for wclose packets.
        """

    def waction(self, container):
        """
        Hook for waction packets.
        """

    def wacknowledge(self, container):
        """
        Hook for wacknowledge packets.
        """

    def wcreative(self, container):
        """
        Hook for creative inventory action packets.
        """

    def sign(self, container):
        """
        Hook for sign packets.
        """

    def poll(self, container):
        """
        Hook for poll packets.

        By default, queries the parent factory for some data, and replays it
        in a specific format to the requester. The connection is then closed
        at both ends. This functionality is used by Beta 1.8 clients to poll
        servers for status.
        """

        players = len(self.factory.protocols)
        max_players = self.factory.limitConnections or 1000000

        response = u"%s§%d§%d" % (self.motd, players, max_players)
        self.error(response)

    def quit(self, container):
        """
        Hook for quit packets.

        By default, merely logs the quit message and drops the connection.

        Even if the connection is not dropped, it will be lost anyway since
        the client will close the connection. It's better to explicitly let it
        go here than to have zombie protocols.
        """

        log.msg("Client is quitting: %s" % container.message)
        self.transport.loseConnection()

    # Twisted-level data handlers and methods
    # Please don't override these needlessly, as they are pretty solid and
    # shouldn't need to be touched.

    def dataReceived(self, data):
        self.buf += data

        packets, self.buf = parse_packets(self.buf)

        if packets:
            self.resetTimeout()

        for header, payload in packets:
            if header in self.handlers:
                self.handlers[header](payload)
            else:
                log.err("Didn't handle parseable packet %d!" % header)
                log.err(payload)

    def connectionLost(self, reason):
        if self._ping_loop.running:
            self._ping_loop.stop()

    def timeoutConnection(self):
        self.error("Connection timed out")

    # State-change callbacks
    # Feel free to override these, but call them at some point.

    def challenged(self):
        """
        Called when the client has started authentication with the server.
        """

        self.state = STATE_CHALLENGED

    def authenticated(self):
        """
        Called when the client has successfully authenticated with the server.
        """

        self.state = STATE_AUTHENTICATED

        self._ping_loop.start(30)

    # Event callbacks
    # These are meant to be overriden.

    def orientation_changed(self):
        """
        Called when the client moves.

        This callback is only for orientation, not position.
        """

        pass

    def position_changed(self):
        """
        Called when the client moves.

        This callback is only for position, not orientation.
        """

        pass

    # Convenience methods for consolidating code and expressing intent. I
    # hear that these are occasionally useful. If a method in this section can
    # be used, then *PLEASE* use it; not using it is the same as open-coding
    # whatever you're doing, and only hurts in the long run.

    def write_packet(self, header, **payload):
        """
        Send a packet to the client.
        """

        self.transport.write(make_packet(header, **payload))

    def update_ping(self):
        """
        Send a keepalive to the client.
        """

        timestamp = timestamp_from_clock(reactor)
        self.write_packet("ping", pid=timestamp)

    def update_location(self):
        """
        Send this client's location to the client.

        Also let other clients know where this client is.
        """

        # Don't bother trying to update things if the position's not yet
        # synchronized. We could end up jettisoning them into the void.
        if self.state != STATE_LOCATED:
            return

        x, y, z = self.location.pos
        yaw, pitch = self.location.ori.to_fracs()

        # Inform everybody of our new location.
        packet = make_packet("teleport",
                             eid=self.player.eid,
                             x=x,
                             y=y,
                             z=z,
                             yaw=yaw,
                             pitch=pitch)
        self.factory.broadcast_for_others(packet, self)

        # Inform ourselves of our new location.
        packet = self.location.save_to_packet()
        self.transport.write(packet)

    def ascend(self, count):
        """
        Ascend to the next XZ-plane.

        ``count`` is the number of ascensions to perform, and may be zero in
        order to force this player to not be standing inside a block.

        :returns: bool of whether the ascension was successful

        This client must be located for this method to have any effect.
        """

        if self.state != STATE_LOCATED:
            return False

        x, y, z = self.location.pos.to_block()

        bigx, smallx, bigz, smallz = split_coords(x, z)

        chunk = self.chunks[bigx, bigz]
        column = chunk.get_column(smallx, smallz)

        # Special case: Ascend at most once, if the current spot isn't good.
        if count == 0:
            if not column[y] or column[y + 1] or column[y + 2]:
                # Yeah, we're gonna need to move.
                count += 1
            else:
                # Nope, we're fine where we are.
                return True

        for i in xrange(y, 126):
            # Find the next spot above us which has a platform and two empty
            # blocks of air.
            if column[i] and not column[i + 1] and not column[i + 2]:
                count -= 1
                if not count:
                    break
        else:
            return False

        self.location.pos = self.location.pos._replace(y=i * 32)
        return True

    def error(self, message):
        """
        Error out.

        This method sends ``message`` to the client as a descriptive error
        message, then closes the connection.
        """

        self.transport.write(make_error_packet(message))
        self.transport.loseConnection()

    def play_notes(self, notes):
        """
        Play some music.

        Send a sequence of notes to the player. ``notes`` is a finite iterable
        of pairs of instruments and pitches.

        There is no way to time notes; if staggered playback is desired (and
        it usually is!), then ``play_notes()`` should be called repeatedly at
        the appropriate times.

        This method turns the block beneath the player into a note block,
        plays the requested notes through it, then turns it back into the
        original block, all without actually modifying the chunk.
        """

        x, y, z = self.location.pos.to_block()

        if y:
            y -= 1

        bigx, smallx, bigz, smallz = split_coords(x, z)

        if (bigx, bigz) not in self.chunks:
            return

        block = self.chunks[bigx, bigz].get_block((smallx, y, smallz))
        meta = self.chunks[bigx, bigz].get_metadata((smallx, y, smallz))

        self.write_packet("block",
                          x=x,
                          y=y,
                          z=z,
                          type=blocks["note-block"].slot,
                          meta=0)

        for instrument, pitch in notes:
            self.write_packet("note",
                              x=x,
                              y=y,
                              z=z,
                              pitch=pitch,
                              instrument=instrument)

        self.write_packet("block", x=x, y=y, z=z, type=block, meta=meta)

    # Automatic properties. Assigning to them causes the client to be notified
    # of changes.

    @property
    def health(self):
        return self._health

    @health.setter
    def health(self, value):
        if not 0 <= value <= 20:
            raise BetaClientError("Invalid health value %d" % value)

        if self._health != value:
            self.write_packet("health", hp=value, fp=0, saturation=0)
            self._health = value

    @property
    def latency(self):
        return self._latency

    @latency.setter
    def latency(self, value):
        # Clamp the value to not exceed the boundaries of the packet. This is
        # necessary even though, in theory, a ping this high is bad news.
        value = clamp(value, 0, 65535)

        # Check to see if this is a new value, and if so, alert everybody.
        if self._latency != value:
            packet = make_packet("players",
                                 name=self.username,
                                 online=True,
                                 ping=value)
            self.factory.broadcast(packet)
            self._latency = value