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)
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)
def test_perfect_diagonal_3d_negative(self): src = Location() src.x, src.y, src.z = 0, 0, 0 dest = Location() dest.x, dest.y, dest.z = -3, -3, -3 coords = [(0, 0, 0), (-1, -1, -1), (-2, -2, -2), (-3, -3, -3)] self.assertEqual(coords, list(gen_line_simple(src, dest)))
def test_perfect_diagonal_3d(self): src = Location() src.x, src.y, src.z = 0, 0, 0 dest = Location() dest.x, dest.y, dest.z = 3, 3, 3 coords = [(0, 0, 0), (1, 1, 1), (2, 2, 2), (3, 3, 3)] self.assertEqual(coords, list(gen_line_simple(src, dest)))
def test_straight_line(self): src = Location() src.x, src.y, src.z = 0, 0, 0 dest = Location() dest.x, dest.y, dest.z = 0, 0, 3 coords = [(0, 0, 0), (0, 0, 1), (0, 0, 2), (0, 0, 3)] self.assertEqual(coords, list(gen_line_simple(src, dest)))
def move_to(self, x, z): """ Move to the place. Don't do pathfinding. """ l = Location() l.x = x l.z = z g = self.change_movement(l, None, 3) self.cmd_queue.append(g)
def test_straight_line_float(self): """ If floating-point coordinates are used, the algorithm still considers only integer coordinates and outputs floored coordinates. """ src = Location() src.x, src.y, src.z = 0, 0, 0.5 dest = Location() dest.x, dest.y, dest.z = 0, 0, 3 coords = [(0, 0, 0), (0, 0, 1), (0, 0, 2), (0, 0, 3)] self.assertEqual(coords, list(gen_line_simple(src, dest)))
def _load_entity_from_tag(self, tag): position = tag["Pos"].tags rotation = tag["Rotation"].tags location = Location() location.pos = Position(position[0].value, position[1].value, position[2].value) location.ori = Orientation.from_degs(rotation[0].value, rotation[1].value) location.grounded = bool(tag["OnGround"]) entity = entities[tag["id"].value](location=location) self._entity_loaders[entity.name](entity, tag) return entity
def move_random(self): """ Just move the bot around... There are no tests about the world geometry, etc """ l = Location() l.x = self.location.x + 2 - random.random() * 4 l.z = self.location.z + 2 - random.random() * 4 g = self.change_movement(l, None, 5) self.cmd_queue.append(g)
def create_entity(self, x, y, z, name, **kwargs): """ Spawn an entirely new entity at the specified block coordinates. Handles entity registration as well as instantiation. """ bigx = x // 16 bigz = z // 16 location = Location.at_block(x, y, z) entity = entities[name](eid=0, location=location, **kwargs) self.register_entity(entity) d = self.world.request_chunk(bigx, bigz) @d.addCallback def cb(chunk): chunk.entities.add(entity) log.msg("Created entity %s" % entity) # XXX Maybe just send the entity object to the manager instead of # the following? if hasattr(entity,'loop'): self.world.mob_manager.start_mob(entity) return entity
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()
def create_entity(self, x, y, z, name, **kwargs): """ Spawn an entirely new entity at the specified block coordinates. Handles entity registration as well as instantiation. """ bigx = x // 16 bigz = z // 16 location = Location.at_block(x, y, z) entity = entities[name](eid=0, location=location, **kwargs) self.register_entity(entity) d = self.world.request_chunk(bigx, bigz) @d.addCallback def cb(chunk): chunk.entities.add(entity) log.msg("Created entity %s" % entity) # XXX Maybe just send the entity object to the manager instead of # the following? if hasattr(entity, 'loop'): self.world.mob_manager.start_mob(entity) return entity
def __init__(self): self.chunks = dict() self.windows = dict() self.wid = 1 self.location = Location() self.handlers = { 0: self.ping, 1: self.login, 2: self.handshake, 3: self.chat, 7: self.use, 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, 104: self.inventory, 106: self.wacknowledge, 130: self.sign, 255: self.quit, } self._ping_loop = LoopingCall(self.update_ping)
def change_movement(self, location, position, t): """ give a request to the bot to move somewhere or position itself return a generator that will return packets so that the movement is smooth. the packets will be written to the wire at regular intervals. NOTE: wouldn't it be real nice if we implimented position as a vector? Would look a lot better if plugging in some real math. splines, anyone? """ this_loc = Location() this_loc.x = self.location.x this_loc.z = self.location.z steps = float(t) / float(self.conn.bot_tick_interval) x_origin = self.location.x x_offset = 0 x_dist = location.x - self.location.x x_step = x_dist / steps z_origin = self.location.z z_offset = 0 z_dist = location.z - self.location.z z_step = z_dist / steps arrived = False while arrived == False: if abs(x_offset) < abs(x_dist): x_offset += x_step this_loc.x = x_origin + x_offset else: arrived = True if abs(z_offset) < abs(z_dist): z_offset += z_step this_loc.z = z_origin + z_offset arrived = False self.location.x = this_loc.x self.location.z = this_loc.z p, l, f = self.location.build_containers() yield make_packet("position", position=p, flying=f)
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 TestLocationMethods(unittest.TestCase): def setUp(self): self.l = Location() def test_trivial(self): pass def test_in_front_of(self): other = self.l.in_front_of(1) self.assertEqual(other.x, 0) self.assertEqual(other.z, 1) def test_in_front_of_yaw(self): self.l.yaw = 90 other = self.l.in_front_of(1) self.assertEqual(other.x, -1) self.assertEqual(other.z, 0) test_in_front_of_yaw.todo = "Precision problems"
def __init__(self): self.chunks = dict() self.windows = {} self.wid = 1 self.location = Location() self.handlers = { 0x00: self.ping, 0x02: self.handshake, 0x03: self.chat, 0x07: self.use, 0x09: self.respawn, 0x0a: self.grounded, 0x0b: self.position, 0x0c: self.orientation, 0x0d: self.location_packet, 0x0e: self.digging, 0x0f: self.build, 0x10: self.equip, 0x12: self.animate, 0x13: self.action, 0x15: self.pickup, 0x65: self.wclose, 0x66: self.waction, 0x6a: self.wacknowledge, 0x6b: self.wcreative, 0x82: self.sign, 0xca: self.client_settings, 0xcb: self.complete, 0xcc: self.settings_packet, 0xfe: self.poll, 0xff: self.quit, } self._ping_loop = LoopingCall(self.update_ping) self.setTimeout(30)
def create_entity(self, x, y, z, name, **kwargs): """ Spawn an entirely new entity. Handles entity registration as well as instantiation. """ location = Location() location.x = x location.y = y location.z = z entity = entities[name](eid=0, location=location, **kwargs) self.register_entity(entity) bigx = entity.location.x // 16 bigz = entity.location.z // 16 d = self.world.request_chunk(bigx, bigz) d.addCallback(lambda chunk: chunk.entities.add(entity)) d.addCallback(lambda none: log.msg("Created entity %s" % entity)) return entity
def _load_entity_from_tag(self, tag): location = Location() position = tag["Pos"].tags rotation = tag["Rotation"].tags location.x = position[0].value location.y = position[1].value location.z = position[2].value location.yaw = rotation[0].value location.pitch = rotation[1].value location.grounded = bool(tag["OnGround"]) entity = entities[tag["id"].value](location=location) self._entity_loaders[entity.name](entity, tag) return entity
def __init__(self, location=None, eid=0, **kwargs): """ Create an entity. This method calls super(). """ super(Entity, self).__init__() self.eid = eid if location is None: self.location = Location() else: self.location = location
def create_entity(self, x, y, z, name, **kwargs): """ Spawn an entirely new entity. Handles entity registration as well as instantiation. """ location = Location() location.x = x location.y = y location.z = z entity = entities[name](eid=0, location=location, **kwargs) self.register_entity(entity) bigx = entity.location.x // 16 bigz = entity.location.z // 16 d = self.world.request_chunk(bigx, bigz) d.addCallback(lambda chunk: chunk.entities.add(entity)) d.addCallback(lambda none: log.msg("Created entity %s" % entity)) if hasattr(entity,'loop'): # XXX Maybe just send the entity object to the manager? self.world.mob_manager.start_mob(entity) return entity
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)
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)
def test_at_block(self): l = Location.at_block(3, 4, 5) self.assertEqual(l.pos.x, 96) self.assertEqual(l.pos.y, 128) self.assertEqual(l.pos.z, 160)
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
def setUp(self): self.l = Location()
def test_distance(self): other = Location() other.x = 2 other.y = 3 other.z = 6 self.assertEqual(self.l.distance(other), 7)
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