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)
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)
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()
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)
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)
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
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