def authenticated(self): BetaServerProtocol.authenticated(self) # Init player, and copy data into it. self.player = yield self.factory.world.load_player(self.username) self.player.eid = self.eid self.location = self.player.location # Init players' inventory window. self.inventory = InventoryWindow(self.player.inventory) # *Now* we are in our factory's list of protocols. Be aware. self.factory.protocols[self.username] = self # Announce our presence. self.factory.chat("%s is joining the game..." % self.username) packet = make_packet("players", name=self.username, online=True, ping=0) self.factory.broadcast(packet) # Craft our avatar and send it to already-connected other players. packet = make_packet("create", eid=self.player.eid) packet += self.player.save_to_packet() self.factory.broadcast_for_others(packet, self) # And of course spawn all of those players' avatars in our client as # well. for protocol in self.factory.protocols.itervalues(): # Skip over ourselves; otherwise, the client tweaks out and # usually either dies or locks up. if protocol is self: continue self.write_packet("create", eid=protocol.player.eid) packet = protocol.player.save_to_packet() packet += protocol.player.save_equipment_to_packet() self.transport.write(packet) # Send spawn and inventory. spawn = self.factory.world.level.spawn packet = make_packet("spawn", x=spawn[0], y=spawn[1], z=spawn[2]) packet += self.inventory.save_to_packet() self.transport.write(packet) # TODO: Send Abilities (0xca) # TODO: Update Health (0x08) # TODO: Update Experience (0x2b) # Send weather. self.transport.write(self.factory.vane.make_packet()) self.send_initial_chunk_and_location() self.time_loop = LoopingCall(self.update_time) self.time_loop.start(10)
def authenticated(self): BetaServerProtocol.authenticated(self) # Init player, and copy data into it. self.player = yield self.factory.world.load_player(self.username) self.player.eid = self.eid self.location = self.player.location # Init players' inventory window. self.inventory = InventoryWindow(self.player.inventory) # Announce our presence. packet = make_packet("chat", message="%s is joining the game..." % self.username) packet += make_packet("players", name=self.username, online=True, ping=0) self.factory.broadcast(packet) # Craft our avatar and send it to already-connected other players. packet = self.player.save_to_packet() packet += make_packet("create", eid=self.player.eid) self.factory.broadcast_for_others(packet, self) # And of course spawn all of those players' avatars in our client as # well. Note that, at this point, we are not listed in the factory's # list of protocols, so we won't accidentally send one of these to # ourselves. for protocol in self.factory.protocols.itervalues(): packet = protocol.player.save_to_packet() packet += protocol.player.save_equipment_to_packet() self.transport.write(packet) self.write_packet("create", eid=protocol.player.eid) # *Now* we are in our factory's list of protocols. Be aware. self.factory.protocols[self.username] = self # Send spawn and inventory. spawn = self.factory.world.spawn packet = make_packet("spawn", x=spawn[0], y=spawn[1], z=spawn[2]) packet += self.inventory.save_to_packet() self.transport.write(packet) # Send weather. self.transport.write(self.factory.vane.make_packet()) self.send_initial_chunk_and_location() self.time_loop = LoopingCall(self.update_time) self.time_loop.start(10)
def authenticated(self): BetaServerProtocol.authenticated(self) # Init player, and copy data into it. self.player = yield self.factory.world.load_player(self.username) self.player.eid = self.eid self.location = self.player.location # Init players' inventory window. self.inventory = InventoryWindow(self.player.inventory) # Announce our presence. packet = make_packet("chat", message="%s is joining the game..." % self.username) packet += make_packet("players", name=self.username, online=True, ping=0) self.factory.broadcast(packet) # Craft our avatar and send it to already-connected other players. packet = self.player.save_to_packet() packet += make_packet("create", eid=self.player.eid) self.factory.broadcast_for_others(packet, self) # And of course spawn all of those players' avatars in our client as # well. Note that, at this point, we are not listed in the factory's # list of protocols, so we won't accidentally send one of these to # ourselves. for protocol in self.factory.protocols.itervalues(): packet = protocol.player.save_to_packet() packet += protocol.player.save_equipment_to_packet() self.transport.write(packet) self.write_packet("create", eid=protocol.player.eid) # *Now* we are in our factory's list of protocols. Be aware. self.factory.protocols[self.username] = self # Send spawn and inventory. spawn = self.factory.world.level.spawn packet = make_packet("spawn", x=spawn[0], y=spawn[1], z=spawn[2]) packet += self.inventory.save_to_packet() self.transport.write(packet) # Send weather. self.transport.write(self.factory.vane.make_packet()) self.send_initial_chunk_and_location() self.time_loop = LoopingCall(self.update_time) self.time_loop.start(10)
class BravoProtocol(BetaServerProtocol): """ A ``BetaServerProtocol`` suitable for serving MC worlds to clients. This protocol really does need to be hooked up with a ``BravoFactory`` or something very much like it. """ chunk_tasks = None time_loop = None eid = 0 last_dig = None def __init__(self, config, name): BetaServerProtocol.__init__(self) self.config = config self.config_name = "world %s" % name # Retrieve the MOTD. Only needs to be done once. self.motd = self.config.getdefault(self.config_name, "motd", "BravoServer") def register_hooks(self): log.msg("Registering client hooks...") plugin_types = { "open_hooks": IWindowOpenHook, "click_hooks": IWindowClickHook, "close_hooks": IWindowCloseHook, "pre_build_hooks": IPreBuildHook, "post_build_hooks": IPostBuildHook, "pre_dig_hooks": IPreDigHook, "dig_hooks": IDigHook, "sign_hooks": ISignHook, "use_hooks": IUseHook, } for t in plugin_types: setattr(self, t, getattr(self.factory, t)) log.msg("Registering policies...") if self.factory.mode == "creative": self.dig_policy = dig_policies["speedy"] else: self.dig_policy = dig_policies["notchy"] log.msg("Registered client plugin hooks!") @inlineCallbacks def authenticated(self): BetaServerProtocol.authenticated(self) # Init player, and copy data into it. self.player = yield self.factory.world.load_player(self.username) self.player.eid = self.eid self.location = self.player.location # Init players' inventory window. self.inventory = InventoryWindow(self.player.inventory) # Announce our presence. packet = make_packet("chat", message="%s is joining the game..." % self.username) packet += make_packet("players", name=self.username, online=True, ping=0) self.factory.broadcast(packet) # Craft our avatar and send it to already-connected other players. packet = self.player.save_to_packet() packet += make_packet("create", eid=self.player.eid) self.factory.broadcast_for_others(packet, self) # And of course spawn all of those players' avatars in our client as # well. Note that, at this point, we are not listed in the factory's # list of protocols, so we won't accidentally send one of these to # ourselves. for protocol in self.factory.protocols.itervalues(): packet = protocol.player.save_to_packet() packet += protocol.player.save_equipment_to_packet() self.transport.write(packet) self.write_packet("create", eid=protocol.player.eid) self.factory.protocols[self.username] = self # Send spawn and inventory. spawn = self.factory.world.spawn packet = make_packet("spawn", x=spawn[0], y=spawn[1], z=spawn[2]) packet += self.inventory.save_to_packet() self.transport.write(packet) # Send weather. self.transport.write(self.factory.vane.make_packet()) self.send_initial_chunk_and_location() self.time_loop = LoopingCall(self.update_time) self.time_loop.start(10) def orientation_changed(self): # Bang your head! packet = make_packet("entity-orientation", eid=self.player.eid, yaw=int(self.location.theta * 255 / (2 * pi)) % 256, pitch=int(self.location.phi * 255 / (2 * pi)) % 256, ) self.factory.broadcast_for_others(packet, self) def position_changed(self): x, chaff, z, chaff = split_coords(self.location.x, self.location.z) # Inform everybody of our new location. packet = make_packet("teleport", eid=self.player.eid, x=self.location.x * 32, y=self.location.y * 32, z=self.location.z * 32, yaw=int(self.location.theta * 255 / (2 * pi)) % 256, pitch=int(self.location.phi * 255 / (2 * pi)) % 256, ) self.factory.broadcast_for_others(packet, self) self.update_chunks() for entity in self.entities_near(2): if entity.name != "Item": continue left = self.player.inventory.add(entity.item, entity.quantity) if left != entity.quantity: if left != 0: # partial collect entity.quantity = left else: packet = make_packet("collect", eid=entity.eid, destination=self.player.eid) packet += make_packet("destroy", eid=entity.eid) self.factory.broadcast(packet) self.factory.destroy_entity(entity) packet = self.inventory.save_to_packet() self.transport.write(packet) def entities_near(self, radius): """ Obtain the entities within a radius of this player. Radius is measured in blocks. """ chunk_radius = int(radius // 16 + 1) chunkx, chaff, chunkz, chaff = split_coords(self.location.x, self.location.z) minx = chunkx - chunk_radius maxx = chunkx + chunk_radius + 1 minz = chunkz - chunk_radius maxz = chunkz + chunk_radius + 1 for x, z in product(xrange(minx, maxx), xrange(minz, maxz)): if (x, z) not in self.chunks: continue chunk = self.chunks[x, z] yieldables = [entity for entity in chunk.entities if self.location.distance(entity.location) <= radius] for i in yieldables: yield i def login(self, container): """ Handle a login packet. This method wraps a login hook which is permitted to do just about anything, as long as it's asynchronous. The hook returns a ``Deferred``, which is chained to authenticate the user or disconnect them depending on the results of the authentication. """ # Check the username. If it's "Player", then the client is almost # certainly cracked, so we'll need to give them a better username. # Thankfully, there's a utility function for finding better usernames. username = container.username if username in self.factory.protocols: for name in username_alternatives(username): if name not in self.factory.protocols: container.username = name break else: self.error("Your username is already taken.") return if container.protocol < SUPPORTED_PROTOCOL: # Kick old clients. self.error("This server doesn't support your ancient client.") return elif container.protocol > SUPPORTED_PROTOCOL: # Kick new clients. self.error("This server doesn't support your newfangled client.") return log.msg("Authenticating client, protocol version %d" % container.protocol) d = self.factory.login_hook(self, container) d.addErrback(lambda *args, **kwargs: self.transport.loseConnection()) d.addCallback(lambda *args, **kwargs: self.authenticated()) def handshake(self, container): if not self.factory.handshake_hook(self, container): self.transport.loseConnection() def chat(self, container): if container.message.startswith("/"): pp = {"factory": self.factory} commands = retrieve_plugins(IChatCommand, parameters=pp) # Register aliases. for plugin in commands.values(): for alias in plugin.aliases: commands[alias] = plugin params = container.message[1:].split(" ") command = params.pop(0).lower() if command and command in commands: def cb(iterable): for line in iterable: self.write_packet("chat", message=line) def eb(error): self.write_packet("chat", message="Error: %s" % error.getErrorMessage()) d = maybeDeferred(commands[command].chat_command, self.username, params) d.addCallback(cb) d.addErrback(eb) else: self.write_packet("chat", message="Unknown command: %s" % command) else: # Send the message up to the factory to be chatified. message = "<%s> %s" % (self.username, container.message) self.factory.chat(message) def use(self, container): """ For each entity in proximity (4 blocks), check if it is the target of this packet and call all hooks that stated interested in this type. """ nearby_players = self.factory.players_near(self.player, 4) for entity in chain(self.entities_near(4), nearby_players): if entity.eid == container.target: for hook in self.use_hooks[entity.name]: hook.use_hook(self.factory, self.player, entity, container.button == 0) break @inlineCallbacks def digging(self, container): if container.x == -1 and container.z == -1 and container.y == 255: # Lala-land dig packet. Discard it for now. return # Player drops currently holding item/block. if (container.state == "dropped" and container.face == "-y" and container.x == 0 and container.y == 0 and container.z == 0): i = self.player.inventory holding = i.holdables[self.player.equipped] if holding: primary, secondary, count = holding if i.consume((primary, secondary), self.player.equipped): dest = self.location.in_front_of(2) dest.y += 1 coords = (int(dest.x * 32) + 16, int(dest.y * 32) + 16, int(dest.z * 32) + 16) self.factory.give(coords, (primary, secondary), 1) # Re-send inventory. packet = self.inventory.save_to_packet() self.transport.write(packet) # If no items in this slot are left, this player isn't # holding an item anymore. if i.holdables[self.player.equipped] is None: packet = make_packet("entity-equipment", eid=self.player.eid, slot=0, primary=65535, secondary=0 ) self.factory.broadcast_for_others(packet, self) return if container.state == "shooting": self.shoot_arrow() return bigx, smallx, bigz, smallz = split_coords(container.x, container.z) coords = smallx, container.y, smallz try: chunk = self.chunks[bigx, bigz] except KeyError: self.error("Couldn't dig in chunk (%d, %d)!" % (bigx, bigz)) return block = chunk.get_block((smallx, container.y, smallz)) if container.state == "started": # Run pre dig hooks for hook in self.pre_dig_hooks: cancel = yield maybeDeferred(hook.pre_dig_hook, self.player, (container.x, container.y, container.z), block) if cancel: return tool = self.player.inventory.holdables[self.player.equipped] # Check to see whether we should break this block. if self.dig_policy.is_1ko(block, tool): self.run_dig_hooks(chunk, coords, blocks[block]) else: # Set up a timer for breaking the block later. dtime = time() + self.dig_policy.dig_time(block, tool) self.last_dig = coords, block, dtime elif container.state == "stopped": # The client thinks it has broken a block. We shall see. if not self.last_dig: return oldcoords, oldblock, dtime = self.last_dig if oldcoords != coords or oldblock != block: # Nope! self.last_dig = None return dtime -= time() # When enough time has elapsed, run the dig hooks. d = deferLater(reactor, max(dtime, 0), self.run_dig_hooks, chunk, coords, blocks[block]) d.addCallback(lambda none: setattr(self, "last_dig", None)) def run_dig_hooks(self, chunk, coords, block): """ Destroy a block and run the post-destroy dig hooks. """ x, y, z = coords l = [] for hook in self.dig_hooks: l.append(maybeDeferred(hook.dig_hook, chunk, x, y, z, block)) if block.breakable: chunk.destroy(coords) dl = DeferredList(l) dl.addCallback(lambda none: self.factory.flush_chunk(chunk)) @inlineCallbacks def build(self, container): if container.x == -1 and container.z == -1 and container.y == 255: # Lala-land build packet. Discard it for now. return # Is the target being selected? bigx, smallx, bigz, smallz = split_coords(container.x, container.z) try: chunk = self.chunks[bigx, bigz] except KeyError: self.error("Couldn't select in chunk (%d, %d)!" % (bigx, bigz)) return # Try to open it first for hook in self.open_hooks: window = yield maybeDeferred(hook.open_hook, self, container, chunk.get_block((smallx, container.y, smallz))) if window: self.write_packet("window-open", wid=window.wid, type=window.identifier, title=window.title, slots=window.slots_num) packet = window.save_to_packet() self.transport.write(packet) # window opened return # Ignore clients that think -1 is placeable. if container.primary == -1: return # Special case when face is "noop": Update the status of the currently # held block rather than placing a new block. if container.face == "noop": return if container.primary in blocks: block = blocks[container.primary] elif container.primary in items: block = items[container.primary] else: log.err("Ignoring request to place unknown block %d" % container.primary) return # it's the top of the world, you can't build here if container.y == 127 and container.face == '+y': return # Run pre-build hooks. These hooks are able to interrupt the build # process. builddata = BuildData(block, 0x0, container.x, container.y, container.z, container.face) for hook in self.pre_build_hooks: cont, builddata, cancel = yield maybeDeferred(hook.pre_build_hook, self.player, builddata) if cancel: # Flush damaged chunks. for chunk in self.chunks.itervalues(): self.factory.flush_chunk(chunk) return if not cont: break # Run the build. try: yield maybeDeferred(self.run_build, builddata) except BuildError: return newblock = builddata.block.slot coords = adjust_coords_for_face( (builddata.x, builddata.y, builddata.z), builddata.face) # Run post-build hooks. These are merely callbacks which cannot # interfere with the build process, largely because the build process # already happened. for hook in self.post_build_hooks: yield maybeDeferred(hook.post_build_hook, self.player, coords, builddata.block) # Feed automatons. for automaton in self.factory.automatons: if newblock in automaton.blocks: automaton.feed(coords) # Re-send inventory. # XXX this could be optimized if/when inventories track damage. packet = self.inventory.save_to_packet() self.transport.write(packet) # Flush damaged chunks. for chunk in self.chunks.itervalues(): self.factory.flush_chunk(chunk) def run_build(self, builddata): block, metadata, x, y, z, face = builddata # Don't place items as blocks. if block.slot not in blocks: raise BuildError("Couldn't build item %r as block" % block) # Check for orientable blocks. if not metadata and block.orientable(): metadata = block.orientation(face) if metadata is None: # Oh, I guess we can't even place the block on this face. raise BuildError("Couldn't orient block %r on face %s" % (block, face)) # Make sure we can remove it from the inventory first. if not self.player.inventory.consume((block.slot, 0), self.player.equipped): # Okay, first one was a bust; maybe we can consume the related # block for dropping instead? if not self.player.inventory.consume(block.drop, self.player.equipped): raise BuildError("Couldn't consume %r from inventory" % block) # Offset coords according to face. x, y, z = adjust_coords_for_face((x, y, z), face) # Set the block and data. dl = [self.factory.world.set_block((x, y, z), block.slot)] if metadata: dl.append(self.factory.world.set_metadata((x, y, z), metadata)) return DeferredList(dl) def equip(self, container): self.player.equipped = container.item # Inform everyone about the item the player is holding now. item = self.player.inventory.holdables[self.player.equipped] if item is None: # Empty slot. Use signed short -1 == unsigned 65535. primary, secondary = 65535, 0 else: primary, secondary, count = item packet = make_packet("entity-equipment", eid=self.player.eid, slot=0, primary=primary, secondary=secondary ) self.factory.broadcast_for_others(packet, self) def pickup(self, container): self.factory.give((container.x, container.y, container.z), (container.primary, container.secondary), container.count) def animate(self, container): # Broadcast the animation of the entity to everyone else. Only swing # arm is send by notchian clients. packet = make_packet("animate", eid=self.player.eid, animation=container.animation ) self.factory.broadcast_for_others(packet, self) @inlineCallbacks def wclose(self, container): # run all hooks for hook in self.close_hooks: yield maybeDeferred(hook.close_hook, self, container) @inlineCallbacks def waction(self, container): # run hooks until handled handled = False for hook in self.click_hooks: res = yield maybeDeferred(hook.click_hook, self, container) handled = handled or res self.write_packet("window-token", wid=container.wid, token=container.token, acknowledged=handled) def creative_inventory(self, container): # apply inventory change that was done in creative mode applied = self.inventory.creative(container.slot, container.itemid, container.damage, container.quantity) if applied: # Inform other players about changes to this player's equipment. equipped_slot = self.player.equipped + 36 if container.slot == equipped_slot: packet = make_packet("entity-equipment", eid=self.player.eid, slot=0, primary=container.itemid, secondary=container.damage ) self.factory.broadcast_for_others(packet, self) def shoot_arrow(self): # TODO 1. Create arrow entity: arrow = Arrow(self.factory, self.player) # 2. Register within the factory: self.factory.register_entity(arrow) # 3. Run it: arrow.run() pass def sign(self, container): bigx, smallx, bigz, smallz = split_coords(container.x, container.z) try: chunk = self.chunks[bigx, bigz] except KeyError: self.error("Couldn't handle sign in chunk (%d, %d)!" % (bigx, bigz)) return if (smallx, container.y, smallz) in chunk.tiles: new = False s = chunk.tiles[smallx, container.y, smallz] else: new = True s = Sign(smallx, container.y, smallz) chunk.tiles[smallx, container.y, smallz] = s s.text1 = container.line1 s.text2 = container.line2 s.text3 = container.line3 s.text4 = container.line4 chunk.dirty = True # The best part of a sign isn't making one, it's showing everybody # else on the server that you did. packet = make_packet("sign", container) self.factory.broadcast_for_chunk(packet, bigx, bigz) # Run sign hooks. for hook in self.sign_hooks: hook.sign_hook(self.factory, chunk, container.x, container.y, container.z, [s.text1, s.text2, s.text3, s.text4], new) def disable_chunk(self, x, z): # Remove the chunk from cache. chunk = self.chunks.pop(x, z) for entity in chunk.entities: self.write_packet("destroy", eid=entity.eid) self.write_packet("prechunk", x=x, z=z, enabled=0) def enable_chunk(self, x, z): """ Request a chunk. This function will asynchronously obtain the chunk, and send it on the wire. :returns: `Deferred` that will be fired when the chunk is obtained, with no arguments """ if (x, z) in self.chunks: return succeed(None) d = self.factory.world.request_chunk(x, z) d.addCallback(self.send_chunk) return d def send_chunk(self, chunk): self.write_packet("prechunk", x=chunk.x, z=chunk.z, enabled=1) packet = chunk.save_to_packet() self.transport.write(packet) for entity in chunk.entities: packet = entity.save_to_packet() self.transport.write(packet) for entity in chunk.tiles.itervalues(): if entity.name == "Sign": packet = entity.save_to_packet() self.transport.write(packet) self.chunks[chunk.x, chunk.z] = chunk def send_initial_chunk_and_location(self): bigx, smallx, bigz, smallz = split_coords(self.location.x, self.location.z) # Spawn the 25 chunks in a square around the spawn, *before* spawning # the player. Otherwise, there's a funky Beta 1.2 bug which causes the # player to not be able to move. d = cooperate( self.enable_chunk(i, j) for i, j in product( xrange(bigx - 3, bigx + 3), xrange(bigz - 3, bigz + 3) ) ).whenDone() # Don't dare send more chunks beyond the initial one until we've # spawned. d.addCallback(lambda none: self.update_location()) d.addCallback(lambda none: self.position_changed()) # Send the MOTD. if self.motd: @d.addCallback def cb(none): self.write_packet("chat", message=self.motd.replace("<tagline>", get_motd())) # Finally, start the secondary chunk loop. d.addCallback(lambda none: self.update_chunks()) def update_location(self): bigx, smallx, bigz, smallz = split_coords(self.location.x, self.location.z) chunk = self.chunks[bigx, bigz] height = chunk.height_at(smallx, smallz) + 2 self.location.y = height packet = self.location.save_to_packet() self.transport.write(packet) def update_chunks(self): x, chaff, z, chaff = split_coords(self.location.x, self.location.z) new = set((i + x, j + z) for i, j in circle) old = set(self.chunks.iterkeys()) added = new - old discarded = old - new # Perhaps some explanation is in order. # The cooperate() function iterates over the iterable it is fed, # without tying up the reactor, by yielding after each iteration. The # inner part of the generator expression generates all of the chunks # around the currently needed chunk, and it sorts them by distance to # the current chunk. The end result is that we load chunks one-by-one, # nearest to furthest, without stalling other clients. if self.chunk_tasks: for task in self.chunk_tasks: try: task.stop() except (TaskDone, TaskFailed): pass self.chunk_tasks = [cooperate(task) for task in ( self.enable_chunk(i, j) for i, j in sorted(added, key=lambda t: (t[0] - x)**2 + (t[1] - z)**2) ), (self.disable_chunk(i, j) for i, j in discarded) ] def update_time(self): self.write_packet("time", timestamp=int(self.factory.time)) def connectionLost(self, reason): """ Cleanup after a lost connection. Most of the time, these connections are lost cleanly; we don't have any cleanup to do in the unclean case since clients don't have any kind of pending state which must be recovered. Remember, the connection can be lost before identification and authentication, so ``self.username`` and ``self.player`` can be None. """ if self.username and self.player: self.factory.world.save_player(self.username, self.player) if self.player: self.factory.destroy_entity(self.player) packet = make_packet("destroy", eid=self.player.eid) self.factory.broadcast(packet) if self.username: packet = make_packet("players", name=self.username, online=False, ping=0) self.factory.broadcast(packet) self.factory.chat("%s has left the game." % self.username) self.factory.teardown_protocol(self) # We are now torn down. After this point, there will be no more # factory stuff, just our own personal stuff. del self.factory if self.time_loop: self.time_loop.stop() if self.chunk_tasks: for task in self.chunk_tasks: try: task.stop() except (TaskDone, TaskFailed): pass
class BravoProtocol(BetaServerProtocol): """ A ``BetaServerProtocol`` suitable for serving MC worlds to clients. This protocol really does need to be hooked up with a ``BravoFactory`` or something very much like it. """ chunk_tasks = None time_loop = None eid = 0 last_dig = None def __init__(self, config, name): BetaServerProtocol.__init__(self) self.config = config self.config_name = "world %s" % name # Retrieve the MOTD. Only needs to be done once. self.motd = self.config.getdefault(self.config_name, "motd", "BravoServer") def register_hooks(self): log.msg("Registering client hooks...") plugin_types = { "open_hooks": IWindowOpenHook, "click_hooks": IWindowClickHook, "close_hooks": IWindowCloseHook, "pre_build_hooks": IPreBuildHook, "post_build_hooks": IPostBuildHook, "pre_dig_hooks": IPreDigHook, "dig_hooks": IDigHook, "sign_hooks": ISignHook, "use_hooks": IUseHook, } for t in plugin_types: setattr(self, t, getattr(self.factory, t)) log.msg("Registering policies...") if self.factory.mode == "creative": self.dig_policy = dig_policies["speedy"] else: self.dig_policy = dig_policies["notchy"] log.msg("Registered client plugin hooks!") def pre_handshake(self): """ Set up username and get going. """ if self.username in self.factory.protocols: # This username's already taken; find a new one. for name in username_alternatives(username): if name not in self.factory.protocols: container.username = name break else: self.error("Your username is already taken.") return False return True @inlineCallbacks def authenticated(self): BetaServerProtocol.authenticated(self) # Init player, and copy data into it. self.player = yield self.factory.world.load_player(self.username) self.player.eid = self.eid self.location = self.player.location # Init players' inventory window. self.inventory = InventoryWindow(self.player.inventory) # *Now* we are in our factory's list of protocols. Be aware. self.factory.protocols[self.username] = self # Announce our presence. self.factory.chat("%s is joining the game..." % self.username) packet = make_packet("players", name=self.username, online=True, ping=0) self.factory.broadcast(packet) # Craft our avatar and send it to already-connected other players. packet = make_packet("create", eid=self.player.eid) packet += self.player.save_to_packet() self.factory.broadcast_for_others(packet, self) # And of course spawn all of those players' avatars in our client as # well. for protocol in self.factory.protocols.itervalues(): # Skip over ourselves; otherwise, the client tweaks out and # usually either dies or locks up. if protocol is self: continue self.write_packet("create", eid=protocol.player.eid) packet = protocol.player.save_to_packet() packet += protocol.player.save_equipment_to_packet() self.transport.write(packet) # Send spawn and inventory. spawn = self.factory.world.level.spawn packet = make_packet("spawn", x=spawn[0], y=spawn[1], z=spawn[2]) packet += self.inventory.save_to_packet() self.transport.write(packet) # Send weather. self.transport.write(self.factory.vane.make_packet()) self.send_initial_chunk_and_location() self.time_loop = LoopingCall(self.update_time) self.time_loop.start(10) def orientation_changed(self): # Bang your head! yaw, pitch = self.location.ori.to_fracs() packet = make_packet("entity-orientation", eid=self.player.eid, yaw=yaw, pitch=pitch) self.factory.broadcast_for_others(packet, self) def position_changed(self): # Send chunks. self.update_chunks() for entity in self.entities_near(2): if entity.name != "Item": continue left = self.player.inventory.add(entity.item, entity.quantity) if left != entity.quantity: if left != 0: # partial collect entity.quantity = left else: packet = make_packet("collect", eid=entity.eid, destination=self.player.eid) packet += make_packet("destroy", count=1, eid=[entity.eid]) self.factory.broadcast(packet) self.factory.destroy_entity(entity) packet = self.inventory.save_to_packet() self.transport.write(packet) def entities_near(self, radius): """ Obtain the entities within a radius of this player. Radius is measured in blocks. """ chunk_radius = int(radius // 16 + 1) chunkx, chaff, chunkz, chaff = split_coords(self.location.pos.x, self.location.pos.z) minx = chunkx - chunk_radius maxx = chunkx + chunk_radius + 1 minz = chunkz - chunk_radius maxz = chunkz + chunk_radius + 1 for x, z in product(xrange(minx, maxx), xrange(minz, maxz)): if (x, z) not in self.chunks: continue chunk = self.chunks[x, z] yieldables = [entity for entity in chunk.entities if self.location.distance(entity.location) <= (radius * 32)] for i in yieldables: yield i def chat(self, container): if container.message.startswith("/"): pp = {"factory": self.factory} commands = retrieve_plugins(IChatCommand, factory=self.factory) # Register aliases. for plugin in commands.values(): for alias in plugin.aliases: commands[alias] = plugin params = container.message[1:].split(" ") command = params.pop(0).lower() if command and command in commands: def cb(iterable): for line in iterable: self.write_packet("chat", message=line) def eb(error): self.write_packet("chat", message="Error: %s" % error.getErrorMessage()) d = maybeDeferred(commands[command].chat_command, self.username, params) d.addCallback(cb) d.addErrback(eb) else: self.write_packet("chat", message="Unknown command: %s" % command) else: # Send the message up to the factory to be chatified. message = "<%s> %s" % (self.username, container.message) self.factory.chat(message) def use(self, container): """ For each entity in proximity (4 blocks), check if it is the target of this packet and call all hooks that stated interested in this type. """ nearby_players = self.factory.players_near(self.player, 4) for entity in chain(self.entities_near(4), nearby_players): if entity.eid == container.target: for hook in self.use_hooks[entity.name]: hook.use_hook(self.factory, self.player, entity, container.button == 0) break @inlineCallbacks def digging(self, container): if container.x == -1 and container.z == -1 and container.y == 255: # Lala-land dig packet. Discard it for now. return # Player drops currently holding item/block. if (container.state == "dropped" and container.face == "-y" and container.x == 0 and container.y == 0 and container.z == 0): i = self.player.inventory holding = i.holdables[self.player.equipped] if holding: primary, secondary, count = holding if i.consume((primary, secondary), self.player.equipped): dest = self.location.in_front_of(2) coords = dest.pos._replace(y=dest.pos.y + 1) self.factory.give(coords, (primary, secondary), 1) # Re-send inventory. packet = self.inventory.save_to_packet() self.transport.write(packet) # If no items in this slot are left, this player isn't # holding an item anymore. if i.holdables[self.player.equipped] is None: packet = make_packet("entity-equipment", eid=self.player.eid, slot=0, primary=65535, count=1, secondary=0 ) self.factory.broadcast_for_others(packet, self) return if container.state == "shooting": self.shoot_arrow() return bigx, smallx, bigz, smallz = split_coords(container.x, container.z) coords = smallx, container.y, smallz try: chunk = self.chunks[bigx, bigz] except KeyError: self.error("Couldn't dig in chunk (%d, %d)!" % (bigx, bigz)) return block = chunk.get_block((smallx, container.y, smallz)) if container.state == "started": # Run pre dig hooks for hook in self.pre_dig_hooks: cancel = yield maybeDeferred(hook.pre_dig_hook, self.player, (container.x, container.y, container.z), block) if cancel: return tool = self.player.inventory.holdables[self.player.equipped] # Check to see whether we should break this block. if self.dig_policy.is_1ko(block, tool): self.run_dig_hooks(chunk, coords, blocks[block]) else: # Set up a timer for breaking the block later. dtime = time() + self.dig_policy.dig_time(block, tool) self.last_dig = coords, block, dtime elif container.state == "stopped": # The client thinks it has broken a block. We shall see. if not self.last_dig: return oldcoords, oldblock, dtime = self.last_dig if oldcoords != coords or oldblock != block: # Nope! self.last_dig = None return dtime -= time() # When enough time has elapsed, run the dig hooks. d = deferLater(reactor, max(dtime, 0), self.run_dig_hooks, chunk, coords, blocks[block]) d.addCallback(lambda none: setattr(self, "last_dig", None)) def run_dig_hooks(self, chunk, coords, block): """ Destroy a block and run the post-destroy dig hooks. """ x, y, z = coords if block.breakable: chunk.destroy(coords) l = [] for hook in self.dig_hooks: l.append(maybeDeferred(hook.dig_hook, chunk, x, y, z, block)) dl = DeferredList(l) dl.addCallback(lambda none: self.factory.flush_chunk(chunk)) @inlineCallbacks def build(self, container): """ Handle a build packet. Several things must happen. First, the packet's contents need to be examined to ensure that the packet is valid. A check is done to see if the packet is opening a windowed object. If not, then a build is run. """ # Is the target within our purview? We don't do a very strict # containment check, but we *do* require that the chunk be loaded. bigx, smallx, bigz, smallz = split_coords(container.x, container.z) try: chunk = self.chunks[bigx, bigz] except KeyError: self.error("Couldn't select in chunk (%d, %d)!" % (bigx, bigz)) return target = blocks[chunk.get_block((smallx, container.y, smallz))] # If it's a chest, hax. if target.name == "chest": from bravo.policy.windows import Chest w = Chest() self.windows[self.wid] = w w.open() self.write_packet("window-open", wid=self.wid, type=w.identifier, title=w.title, slots=w.slots) self.wid += 1 return elif target.name == "workbench": from bravo.policy.windows import Workbench w = Workbench() self.windows[self.wid] = w w.open() self.write_packet("window-open", wid=self.wid, type=w.identifier, title=w.title, slots=w.slots) self.wid += 1 return # Try to open it first for hook in self.open_hooks: window = yield maybeDeferred(hook.open_hook, self, container, chunk.get_block((smallx, container.y, smallz))) if window: self.write_packet("window-open", wid=window.wid, type=window.identifier, title=window.title, slots=window.slots_num) packet = window.save_to_packet() self.transport.write(packet) # window opened return # Ignore clients that think -1 is placeable. if container.primary == -1: return # Special case when face is "noop": Update the status of the currently # held block rather than placing a new block. if container.face == "noop": return # If the target block is vanishable, then adjust our aim accordingly. if target.vanishes: container.face = "+y" container.y -= 1 if container.primary in blocks: block = blocks[container.primary] elif container.primary in items: block = items[container.primary] else: log.err("Ignoring request to place unknown block %d" % container.primary) return # Run pre-build hooks. These hooks are able to interrupt the build # process. builddata = BuildData(block, 0x0, container.x, container.y, container.z, container.face) for hook in self.pre_build_hooks: cont, builddata, cancel = yield maybeDeferred(hook.pre_build_hook, self.player, builddata) if cancel: # Flush damaged chunks. for chunk in self.chunks.itervalues(): self.factory.flush_chunk(chunk) return if not cont: break # Run the build. try: yield maybeDeferred(self.run_build, builddata) except BuildError: return newblock = builddata.block.slot coords = adjust_coords_for_face( (builddata.x, builddata.y, builddata.z), builddata.face) # Run post-build hooks. These are merely callbacks which cannot # interfere with the build process, largely because the build process # already happened. for hook in self.post_build_hooks: yield maybeDeferred(hook.post_build_hook, self.player, coords, builddata.block) # Feed automatons. for automaton in self.factory.automatons: if newblock in automaton.blocks: automaton.feed(coords) # Re-send inventory. # XXX this could be optimized if/when inventories track damage. packet = self.inventory.save_to_packet() self.transport.write(packet) # Flush damaged chunks. for chunk in self.chunks.itervalues(): self.factory.flush_chunk(chunk) def run_build(self, builddata): block, metadata, x, y, z, face = builddata # Don't place items as blocks. if block.slot not in blocks: raise BuildError("Couldn't build item %r as block" % block) # Check for orientable blocks. if not metadata and block.orientable(): metadata = block.orientation(face) if metadata is None: # Oh, I guess we can't even place the block on this face. raise BuildError("Couldn't orient block %r on face %s" % (block, face)) # Make sure we can remove it from the inventory first. if not self.player.inventory.consume((block.slot, 0), self.player.equipped): # Okay, first one was a bust; maybe we can consume the related # block for dropping instead? if not self.player.inventory.consume(block.drop, self.player.equipped): raise BuildError("Couldn't consume %r from inventory" % block) # Offset coords according to face. x, y, z = adjust_coords_for_face((x, y, z), face) # Set the block and data. dl = [self.factory.world.set_block((x, y, z), block.slot)] if metadata: dl.append(self.factory.world.set_metadata((x, y, z), metadata)) return DeferredList(dl) def equip(self, container): self.player.equipped = container.slot # Inform everyone about the item the player is holding now. item = self.player.inventory.holdables[self.player.equipped] if item is None: # Empty slot. Use signed short -1. primary, secondary = -1, 0 else: primary, secondary, count = item packet = make_packet("entity-equipment", eid=self.player.eid, slot=0, primary=primary, count=1, secondary=secondary ) self.factory.broadcast_for_others(packet, self) def pickup(self, container): self.factory.give((container.x, container.y, container.z), (container.primary, container.secondary), container.count) def animate(self, container): # Broadcast the animation of the entity to everyone else. Only swing # arm is send by notchian clients. packet = make_packet("animate", eid=self.player.eid, animation=container.animation ) self.factory.broadcast_for_others(packet, self) def wclose(self, container): wid = container.wid if wid == 0: # WID 0 is reserved for the client inventory. pass elif wid in self.windows: w = self.windows.pop(wid) w.close() else: self.error("WID %d doesn't exist." % wid) def waction(self, container): wid = container.wid if wid in self.windows: w = self.windows[wid] result = w.action(container.slot, container.button, container.token, container.shift, container.primary) self.write_packet("window-token", wid=wid, token=container.token, acknowledged=result) else: self.error("WID %d doesn't exist." % wid) def wcreative(self, container): """ A slot was altered in creative mode. """ # XXX Sometimes the container doesn't contain all of this information. # What then? applied = self.inventory.creative(container.slot, container.primary, container.secondary, container.count) if applied: # Inform other players about changes to this player's equipment. equipped_slot = self.player.equipped + 36 if container.slot == equipped_slot: packet = make_packet("entity-equipment", eid=self.player.eid, # XXX why 0? why not the actual slot? slot=0, primary=container.primary, count=1, secondary=container.secondary, ) self.factory.broadcast_for_others(packet, self) def shoot_arrow(self): # TODO 1. Create arrow entity: arrow = Arrow(self.factory, self.player) # 2. Register within the factory: self.factory.register_entity(arrow) # 3. Run it: arrow.run() pass def sign(self, container): bigx, smallx, bigz, smallz = split_coords(container.x, container.z) try: chunk = self.chunks[bigx, bigz] except KeyError: self.error("Couldn't handle sign in chunk (%d, %d)!" % (bigx, bigz)) return if (smallx, container.y, smallz) in chunk.tiles: new = False s = chunk.tiles[smallx, container.y, smallz] else: new = True s = Sign(smallx, container.y, smallz) chunk.tiles[smallx, container.y, smallz] = s s.text1 = container.line1 s.text2 = container.line2 s.text3 = container.line3 s.text4 = container.line4 chunk.dirty = True # The best part of a sign isn't making one, it's showing everybody # else on the server that you did. packet = make_packet("sign", container) self.factory.broadcast_for_chunk(packet, bigx, bigz) # Run sign hooks. for hook in self.sign_hooks: hook.sign_hook(self.factory, chunk, container.x, container.y, container.z, [s.text1, s.text2, s.text3, s.text4], new) def complete(self, container): """ Attempt to tab-complete user names. """ needle = container.autocomplete usernames = self.factory.protocols.keys() results = complete(needle, usernames) self.write_packet("tab", autocomplete=results) def settings_packet(self, container): """ Acknowledge a change of settings and update chunk distance. """ super(BravoProtocol, self).settings_packet(container) self.update_chunks() def disable_chunk(self, x, z): key = x, z log.msg("Disabling chunk %d, %d" % key) if key not in self.chunks: log.msg("...But the chunk wasn't loaded!") return # Remove the chunk from cache. chunk = self.chunks.pop(key) eids = [e.eid for e in chunk.entities] self.write_packet("destroy", count=len(eids), eid=eids) # Clear chunk data on the client. self.write_packet("chunk", x=x, z=z, continuous=False, primary=0x0, add=0x0, data="") def enable_chunk(self, x, z): """ Request a chunk. This function will asynchronously obtain the chunk, and send it on the wire. :returns: `Deferred` that will be fired when the chunk is obtained, with no arguments """ log.msg("Enabling chunk %d, %d" % (x, z)) if (x, z) in self.chunks: log.msg("...But the chunk was already loaded!") return succeed(None) d = self.factory.world.request_chunk(x, z) @d.addCallback def cb(chunk): self.chunks[x, z] = chunk return chunk d.addCallback(self.send_chunk) return d def send_chunk(self, chunk): log.msg("Sending chunk %d, %d" % (chunk.x, chunk.z)) packet = chunk.save_to_packet() self.transport.write(packet) for entity in chunk.entities: packet = entity.save_to_packet() self.transport.write(packet) for entity in chunk.tiles.itervalues(): if entity.name == "Sign": packet = entity.save_to_packet() self.transport.write(packet) def send_initial_chunk_and_location(self): """ Send the initial chunks and location. This method sends more than one chunk; since Beta 1.2, it must send nearly fifty chunks before the location can be safely sent. """ # Disable located hooks. We'll re-enable them at the end. self.state = STATE_AUTHENTICATED log.msg("Initial, position %d, %d, %d" % self.location.pos) x, y, z = self.location.pos.to_block() bigx, smallx, bigz, smallz = split_coords(x, z) # Send the chunk that the player will stand on. The other chunks are # not so important. There *used* to be a bug, circa Beta 1.2, that # required lots of surrounding geometry to be present, but that's been # fixed. d = self.enable_chunk(bigx, bigz) # What to do if we can't load a given chunk? Just kick 'em. d.addErrback(lambda fail: self.error("Couldn't load a chunk... :c")) # Don't dare send more chunks beyond the initial one until we've # spawned. Once we've spawned, set our status to LOCATED and then # update_location() will work. @d.addCallback def located(none): self.state = STATE_LOCATED # Ensure that we're above-ground. self.ascend(0) d.addCallback(lambda none: self.update_location()) d.addCallback(lambda none: self.position_changed()) # Send the MOTD. if self.motd: @d.addCallback def motd(none): self.write_packet("chat", message=self.motd.replace("<tagline>", get_motd())) # Finally, start the secondary chunk loop. d.addCallback(lambda none: self.update_chunks()) def update_chunks(self): # Don't send chunks unless we're located. if self.state != STATE_LOCATED: return x, y, z = self.location.pos.to_block() x, chaff, z, chaff = split_coords(x, z) # These numbers come from a couple spots, including minecraftwiki, but # I verified them experimentally using torches and pillars to mark # distances on each setting. ~ C. distances = { "tiny": 2, "short": 4, "far": 16, } radius = distances.get(self.settings.distance, 8) new = set(circling(x, z, radius)) old = set(self.chunks.iterkeys()) added = new - old discarded = old - new # Perhaps some explanation is in order. # The cooperate() function iterates over the iterable it is fed, # without tying up the reactor, by yielding after each iteration. The # inner part of the generator expression generates all of the chunks # around the currently needed chunk, and it sorts them by distance to # the current chunk. The end result is that we load chunks one-by-one, # nearest to furthest, without stalling other clients. if self.chunk_tasks: for task in self.chunk_tasks: try: task.stop() except (TaskDone, TaskFailed): pass to_enable = sorted_by_distance(added, x, z) self.chunk_tasks = [ cooperate(self.enable_chunk(i, j) for i, j in to_enable), cooperate(self.disable_chunk(i, j) for i, j in discarded), ] def update_time(self): time = int(self.factory.time) self.write_packet("time", timestamp=time, time=time % 24000) def connectionLost(self, reason): """ Cleanup after a lost connection. Most of the time, these connections are lost cleanly; we don't have any cleanup to do in the unclean case since clients don't have any kind of pending state which must be recovered. Remember, the connection can be lost before identification and authentication, so ``self.username`` and ``self.player`` can be None. """ if self.username and self.player: self.factory.world.save_player(self.username, self.player) if self.player: self.factory.destroy_entity(self.player) packet = make_packet("destroy", count=1, eid=[self.player.eid]) self.factory.broadcast(packet) if self.username: packet = make_packet("players", name=self.username, online=False, ping=0) self.factory.broadcast(packet) self.factory.chat("%s has left the game." % self.username) self.factory.teardown_protocol(self) # We are now torn down. After this point, there will be no more # factory stuff, just our own personal stuff. del self.factory if self.time_loop: self.time_loop.stop() if self.chunk_tasks: for task in self.chunk_tasks: try: task.stop() except (TaskDone, TaskFailed): pass
def setUp(self): self.i = InventoryWindow(Inventory())
class TestWindowIntegration(unittest.TestCase): def setUp(self): self.i = InventoryWindow(Inventory()) def test_craft_wood_from_log(self): self.i.inventory.add(bravo.blocks.blocks["log"].key, 1) # Select log from holdables. self.i.select(36) self.assertEqual(self.i.selected, (bravo.blocks.blocks["log"].slot, 0, 1)) # Select log into crafting. self.i.select(1) self.assertEqual(self.i.slots.crafting[0], (bravo.blocks.blocks["log"].slot, 0, 1)) self.assertTrue(self.i.slots.recipe) self.assertEqual(self.i.slots.crafted[0], (bravo.blocks.blocks["wood"].slot, 0, 4)) # Select wood from crafted. self.i.select(0) self.assertEqual(self.i.selected, (bravo.blocks.blocks["wood"].slot, 0, 4)) self.assertEqual(self.i.slots.crafting[0], None) self.assertEqual(self.i.slots.crafted[0], None) # And select wood into holdables. self.i.select(36) self.assertEqual(self.i.selected, None) self.assertEqual(self.i.inventory.holdables[0], (bravo.blocks.blocks["wood"].slot, 0, 4)) self.assertEqual(self.i.slots.crafting[0], None) self.assertEqual(self.i.slots.crafted[0], None) def test_craft_torches(self): self.i.inventory.add(bravo.blocks.items["coal"].key, 2) self.i.inventory.add(bravo.blocks.items["stick"].key, 2) # Select coal from holdables. self.i.select(36) self.assertEqual(self.i.selected, (bravo.blocks.items["coal"].slot, 0, 2)) # Select coal into crafting. self.i.select(1) self.assertEqual(self.i.slots.crafting[0], (bravo.blocks.items["coal"].slot, 0, 2)) # Select stick from holdables. self.i.select(37) self.assertEqual(self.i.selected, (bravo.blocks.items["stick"].slot, 0, 2)) # Select stick into crafting. self.i.select(3) self.assertEqual(self.i.slots.crafting[2], (bravo.blocks.items["stick"].slot, 0, 2)) self.assertTrue(self.i.slots.recipe) self.assertEqual(self.i.slots.crafted[0], (bravo.blocks.blocks["torch"].slot, 0, 4)) # Select torches from crafted. self.i.select(0) self.assertEqual(self.i.selected, (bravo.blocks.blocks["torch"].slot, 0, 4)) self.i.select(0) self.assertEqual(self.i.selected, (bravo.blocks.blocks["torch"].slot, 0, 8)) self.assertEqual(self.i.slots.crafting[0], None) self.assertEqual(self.i.slots.crafted[0], None) # And select torches into holdables. self.i.select(36) self.assertEqual(self.i.selected, None) self.assertEqual(self.i.inventory.holdables[0], (bravo.blocks.blocks["torch"].slot, 0, 8)) self.assertEqual(self.i.slots.crafting[0], None) self.assertEqual(self.i.slots.crafted[0], None) def test_armor_slots_take_one_item_only(self): self.i.inventory.add((bravo.blocks.items["iron-helmet"].slot, 0), 5) self.i.select(36) self.i.select(5) self.assertEqual(self.i.inventory.armor[0], (bravo.blocks.items["iron-helmet"].slot, 0, 1)) self.assertEqual(self.i.selected, (bravo.blocks.items["iron-helmet"].slot, 0, 4)) # Exchanging one iron-helmet in the armor slot against 5 gold-helmet in the hand # is not possible. self.i.inventory.add((bravo.blocks.items["gold-helmet"].slot, 0), 5) self.i.select(36) self.i.select(5) self.assertEqual(self.i.inventory.armor[0], (bravo.blocks.items["iron-helmet"].slot, 0, 1)) self.assertEqual(self.i.selected, (bravo.blocks.items["gold-helmet"].slot, 0, 5)) def test_armor_slots_take_armor_items_only(self): """ Confirm that dirt cannot be used as a helmet. This is the exact test case from #175. """ self.i.inventory.add((bravo.blocks.blocks["dirt"].slot, 0), 10) self.i.select(36) self.assertFalse(self.i.select(5)) self.assertEqual(self.i.inventory.armor[0], None) self.assertEqual(self.i.selected, (bravo.blocks.blocks["dirt"].slot, 0, 10)) def test_pumpkin_as_helmet(self): self.i.inventory.add((bravo.blocks.blocks["pumpkin"].slot, 0), 1) self.i.select(36) self.i.select(5) self.assertEqual(self.i.inventory.armor[0], (bravo.blocks.blocks["pumpkin"].slot, 0, 1)) self.assertEqual(self.i.selected, None) def test_armor_only_in_matching_slots(self): for index, item in enumerate(["leather-helmet", "chainmail-chestplate", "diamond-leggings", "gold-boots"]): self.i.inventory.add((bravo.blocks.items[item].slot, 0), 1) self.i.select(36) # Can't be placed in other armor slots. other_slots = list(range(4)) other_slots.remove(index) for i in other_slots: self.assertFalse(self.i.select(5 + i)) # But it can in the appropriate slot. self.assertTrue(self.i.select(5 + index)) self.assertEqual(self.i.inventory.armor[index], (bravo.blocks.items[item].slot, 0, 1)) def test_shift_click_crafted(self): # Select log into crafting. self.i.inventory.add(bravo.blocks.blocks["log"].key, 2) self.i.select(36) self.i.select(1) # Shift-Click on wood from crafted. self.i.select(0, False, True) self.assertEqual(self.i.selected, None ) self.assertEqual(self.i.inventory.holdables[8], (bravo.blocks.blocks["wood"].slot, 0, 4)) # Move crafted wood to another slot self.i.select(44) self.i.select(18) # One more time self.i.select(0, False, True) self.assertEqual(self.i.selected, None ) self.assertEqual(self.i.inventory.storage[9], (bravo.blocks.blocks["wood"].slot, 0, 8)) def test_shift_click_crafted_almost_full_inventory(self): # NOTE:Notchian client works this way: you lose items # that was not moved to inventory. So, it's not a bug. # there is space for 3 `wood`s only self.i.inventory.storage[:] = [Slot(1, 0, 64)] * 27 self.i.inventory.holdables[:] = [Slot(bravo.blocks.blocks["wood"].slot, 0, 64)] * 9 self.i.inventory.holdables[1] = Slot(bravo.blocks.blocks["wood"].slot, 0, 63) self.i.inventory.holdables[2] = Slot(bravo.blocks.blocks["wood"].slot, 0, 63) self.i.inventory.holdables[3] = Slot(bravo.blocks.blocks["wood"].slot, 0, 63) # Select log into crafting. self.i.slots.crafting[0] = Slot(bravo.blocks.blocks["log"].slot, 0, 2) self.i.slots.update_crafted() # Shift-Click on wood from crafted. self.assertTrue(self.i.select(0, False, True)) self.assertEqual(self.i.selected, None ) self.assertEqual(self.i.inventory.holdables[1], (bravo.blocks.blocks["wood"].slot, 0, 64)) self.assertEqual(self.i.inventory.holdables[2], (bravo.blocks.blocks["wood"].slot, 0, 64)) self.assertEqual(self.i.inventory.holdables[3], (bravo.blocks.blocks["wood"].slot, 0, 64)) self.assertEqual(self.i.slots.crafting[0], (bravo.blocks.blocks["log"].slot, 0, 1)) self.assertEqual(self.i.slots.crafted[0], (bravo.blocks.blocks["wood"].slot, 0, 4)) def test_shift_click_crafted_full_inventory(self): # there is no space left self.i.inventory.storage[:] = [Slot(1, 0, 64)] * 27 self.i.inventory.holdables[:] = [Slot(bravo.blocks.blocks["wood"].slot, 0, 64)] * 9 # Select log into crafting. self.i.slots.crafting[0] = Slot(bravo.blocks.blocks["log"].slot, 0, 2) self.i.slots.update_crafted() # Shift-Click on wood from crafted. self.assertFalse(self.i.select(0, False, True)) self.assertEqual(self.i.selected, None ) self.assertEqual(self.i.slots.crafting[0], (bravo.blocks.blocks["log"].slot, 0, 2)) def test_close_window(self): items, packets = self.i.close() self.assertEqual(len(items), 0) self.assertEqual(packets, "") self.i.slots.crafting[0] = Slot(bravo.blocks.items["coal"].slot, 0, 1) self.i.slots.crafting[2] = Slot(bravo.blocks.items["stick"].slot, 0, 1) self.i.inventory.storage[0] = Slot(3, 0, 1) # Force crafting table to be rechecked. self.i.slots.update_crafted() self.i.select(9) items, packets = self.i.close() self.assertEqual(self.i.selected, None) self.assertEqual(self.i.slots.crafted[0], None) self.assertEqual(self.i.slots.crafting, [None] * 4) self.assertEqual(len(items), 3) self.assertEqual(items[0], (263, 0, 1)) self.assertEqual(items[1], (280, 0, 1)) self.assertEqual(items[2], (3, 0, 1))
def setUp(self): # like player's inventory window self.i = InventoryWindow(Inventory())
class TestInventoryIntegration(unittest.TestCase): def setUp(self): # like player's inventory window self.i = InventoryWindow(Inventory()) def test_internals(self): self.assertEqual(self.i.metalist, [[None], [None] * 4, [None] * 4, [None] * 27, [None] * 9]) def test_container_resolution(self): c, i = self.i.container_for_slot(0) self.assertTrue(c is self.i.slots.crafted) self.assertEqual(i, 0) c, i = self.i.container_for_slot(2) self.assertTrue(c is self.i.slots.crafting) self.assertEqual(i, 1) c, i = self.i.container_for_slot(7) self.assertTrue(c is self.i.inventory.armor) self.assertEqual(i, 2) c, i = self.i.container_for_slot(18) self.assertTrue(c is self.i.inventory.storage) self.assertEqual(i, 9) c, i = self.i.container_for_slot(44) self.assertTrue(c is self.i.inventory.holdables) self.assertEqual(i, 8) def test_slots_resolution(self): self.assertEqual(self.i.slot_for_container(self.i.slots.crafted, 0), 0) self.assertEqual(self.i.slot_for_container(self.i.slots.crafting, 1), 2) self.assertEqual(self.i.slot_for_container(self.i.slots.storage, 0), -1) self.assertEqual(self.i.slot_for_container(self.i.inventory.armor, 2), 7) self.assertEqual(self.i.slot_for_container(self.i.inventory.storage, 26), 35) self.assertEqual(self.i.slot_for_container(self.i.inventory.holdables, 0), 36) self.assertEqual(self.i.slot_for_container(self.i.slots.crafted, 2), -1) def test_load_holdables_from_list(self): l = [None] * len(self.i) l[36] = 20, 0, 1 self.i.load_from_list(l) self.assertEqual(self.i.inventory.holdables[0], (20, 0, 1)) c, i = self.i.container_for_slot(7) self.assertTrue(c is self.i.inventory.armor) c, i = self.i.container_for_slot(2) self.assertTrue(c is self.i.slots.crafting) def test_select_stack(self): self.i.inventory.holdables[0] = Slot(2, 0, 1) self.i.inventory.holdables[1] = Slot(2, 0, 1) self.i.select(37) self.i.select(36) self.assertEqual(self.i.inventory.holdables[0], (2, 0, 2)) self.assertEqual(self.i.inventory.holdables[1], None) def test_select_switch(self): self.i.inventory.holdables[0] = Slot(2, 0, 1) self.i.inventory.holdables[1] = Slot(3, 0, 1) self.i.select(36) self.i.select(37) self.i.select(36) self.assertEqual(self.i.inventory.holdables[0], (3, 0, 1)) self.assertEqual(self.i.inventory.holdables[1], (2, 0, 1)) def test_select_secondary_switch(self): self.i.inventory.holdables[0] = Slot(2, 0, 1) self.i.inventory.holdables[1] = Slot(3, 0, 1) self.i.select(36) self.i.select(37, True) self.i.select(36, True) self.assertEqual(self.i.inventory.holdables[0], (3, 0, 1)) self.assertEqual(self.i.inventory.holdables[1], (2, 0, 1)) def test_select_outside_window(self): self.assertFalse(self.i.select(64537)) def test_select_secondary(self): self.i.inventory.holdables[0] = Slot(2, 0, 4) self.i.select(36, True) self.assertEqual(self.i.inventory.holdables[0], (2, 0, 2)) self.assertEqual(self.i.selected, (2, 0, 2)) def test_select_secondary_empty(self): for i in range(0, 45): self.assertFalse(self.i.select(i, True)) def test_select_secondary_outside_window(self): """ Test that outrageous selections, such as those generated by clicking outside inventory windows, fail cleanly. """ self.assertFalse(self.i.select(64537), True) def test_select_secondary_selected(self): self.i.selected = Slot(2, 0, 2) self.i.select(36, True) self.assertEqual(self.i.inventory.holdables[0], (2, 0, 1)) self.assertEqual(self.i.selected, (2, 0, 1)) def test_select_secondary_odd(self): self.i.inventory.holdables[0] = Slot(2, 0, 3) self.i.select(36, True) self.assertEqual(self.i.inventory.holdables[0], (2, 0, 1)) self.assertEqual(self.i.selected, (2, 0, 2)) def test_select_fill_up_stack(self): # create two stacks self.i.inventory.holdables[0] = Slot(2, 0, 40) self.i.inventory.holdables[1] = Slot(2, 0, 30) # select first one self.i.select(36) # first slot is now empty - holding 40 items self.assertEqual(self.i.selected, (2, 0, 40)) # second stack is untouched self.assertEqual(self.i.inventory.holdables[1], (2, 0, 30)) # select second stack with left click self.i.select(37) # sums up to more than 64 items - fill up the second stack self.assertEqual(self.i.inventory.holdables[1], (2, 0, 64)) # still hold the left overs self.assertEqual(self.i.selected, (2, 0, 6)) def test_select_secondary_fill_up_stack(self): # create two stacks self.i.inventory.holdables[0] = Slot(2, 0, 40) self.i.inventory.holdables[1] = Slot(2, 0, 30) # select first one self.i.select(36) # first slot is now empty - holding 40 items self.assertEqual(self.i.selected, (2, 0, 40)) # second stack is untouched self.assertEqual(self.i.inventory.holdables[1], (2, 0, 30)) # select second stack with right click self.i.select(37, True) # sums up to more than 64 items self.assertEqual(self.i.inventory.holdables[1], (2, 0, 31)) # still hold the left overs self.assertEqual(self.i.selected, (2, 0, 39)) def test_stacking_items(self): # setup initial items self.i.slots.crafting[0] = Slot(1, 0, 2) self.i.inventory.storage[0] = Slot(2, 0, 1) self.i.inventory.storage[2] = Slot(1, 0, 3) self.i.inventory.holdables[0] = Slot(3, 0 ,1) self.i.inventory.holdables[2] = Slot(1, 0, 62) self.i.inventory.holdables[4] = Slot(1, 0, 4) # shift-LMB on crafting area self.i.select(1, False, True) self.assertEqual(self.i.slots.crafting[0], None) self.assertEqual(self.i.inventory.storage[1], None) self.assertEqual(self.i.inventory.storage[2], (1, 0, 5)) # shift-LMB on storage area self.i.select(11, False, True) self.assertEqual(self.i.inventory.storage[2], None) self.assertEqual(self.i.inventory.holdables[2], (1, 0, 64)) self.assertEqual(self.i.inventory.holdables[4], (1, 0, 7)) # shift-RMB on holdables area self.i.select(38, True, True) self.assertEqual(self.i.inventory.holdables[2], None) self.assertEqual(self.i.inventory.storage[1], (1, 0, 64)) # check if item goes from crafting area directly to # holdables if possible self.i.slots.crafting[1] = Slot(1, 0, 60) self.i.inventory.storage[3] = Slot(1, 0, 63) self.i.select(2, True, True) self.assertEqual(self.i.slots.crafting[1], None) self.assertEqual(self.i.inventory.storage[2], (1, 0, 2)) self.assertEqual(self.i.inventory.storage[3], (1, 0, 64)) self.assertEqual(self.i.inventory.holdables[4], (1, 0, 64)) def test_unstackable_items(self): shovel = (bravo.blocks.items["wooden-shovel"].slot, 0, 1) self.i.inventory.storage[0] = Slot(*shovel) self.i.inventory.storage[1] = Slot(*shovel) self.i.select(9) self.i.select(10) self.assertEqual(self.i.inventory.storage[0], None) self.assertEqual(self.i.inventory.storage[1], shovel) self.assertEqual(self.i.selected, shovel) self.i.select(36) self.i.select(10, False, True) self.assertEqual(self.i.inventory.holdables[0], shovel) self.assertEqual(self.i.inventory.holdables[1], shovel) def test_drop_selected_all(self): self.i.selected = Slot(1, 0, 3) items = self.i.drop_selected() self.assertEqual(self.i.selected, None) self.assertEqual(items, [(1, 0, 3)]) def test_drop_selected_one(self): self.i.selected = Slot(1, 0, 3) items = self.i.drop_selected(True) self.assertEqual(self.i.selected, (1, 0, 2)) self.assertEqual(items, [(1, 0, 1)])
class BravoProtocol(BetaServerProtocol): """ A ``BetaServerProtocol`` suitable for serving MC worlds to clients. This protocol really does need to be hooked up with a ``BravoFactory`` or something very much like it. """ chunk_tasks = None time_loop = None eid = 0 last_dig = None def __init__(self, config, name): BetaServerProtocol.__init__(self) self.config = config self.config_name = "world %s" % name # Retrieve the MOTD. Only needs to be done once. self.motd = self.config.getdefault(self.config_name, "motd", "BravoServer") def register_hooks(self): log.msg("Registering client hooks...") plugin_types = { "open_hooks": IWindowOpenHook, "click_hooks": IWindowClickHook, "close_hooks": IWindowCloseHook, "pre_build_hooks": IPreBuildHook, "post_build_hooks": IPostBuildHook, "pre_dig_hooks": IPreDigHook, "dig_hooks": IDigHook, "sign_hooks": ISignHook, "use_hooks": IUseHook, } for t in plugin_types: setattr(self, t, getattr(self.factory, t)) log.msg("Registering policies...") if self.factory.mode == "creative": self.dig_policy = dig_policies["speedy"] else: self.dig_policy = dig_policies["notchy"] log.msg("Registered client plugin hooks!") @inlineCallbacks def authenticated(self): BetaServerProtocol.authenticated(self) # Init player, and copy data into it. self.player = yield self.factory.world.load_player(self.username) self.player.eid = self.eid self.location = self.player.location # Init players' inventory window. self.inventory = InventoryWindow(self.player.inventory) # Announce our presence. packet = make_packet("chat", message="%s is joining the game..." % self.username) packet += make_packet("players", name=self.username, online=True, ping=0) self.factory.broadcast(packet) # Craft our avatar and send it to already-connected other players. packet = self.player.save_to_packet() packet += make_packet("create", eid=self.player.eid) self.factory.broadcast_for_others(packet, self) # And of course spawn all of those players' avatars in our client as # well. Note that, at this point, we are not listed in the factory's # list of protocols, so we won't accidentally send one of these to # ourselves. for protocol in self.factory.protocols.itervalues(): packet = protocol.player.save_to_packet() packet += protocol.player.save_equipment_to_packet() self.transport.write(packet) self.write_packet("create", eid=protocol.player.eid) # *Now* we are in our factory's list of protocols. Be aware. self.factory.protocols[self.username] = self # Send spawn and inventory. spawn = self.factory.world.spawn packet = make_packet("spawn", x=spawn[0], y=spawn[1], z=spawn[2]) packet += self.inventory.save_to_packet() self.transport.write(packet) # Send weather. self.transport.write(self.factory.vane.make_packet()) self.send_initial_chunk_and_location() self.time_loop = LoopingCall(self.update_time) self.time_loop.start(10) def orientation_changed(self): # Bang your head! yaw, pitch = self.location.ori.to_fracs() packet = make_packet("entity-orientation", eid=self.player.eid, yaw=yaw, pitch=pitch) self.factory.broadcast_for_others(packet, self) def position_changed(self): # Send chunks. self.update_chunks() for entity in self.entities_near(2): if entity.name != "Item": continue left = self.player.inventory.add(entity.item, entity.quantity) if left != entity.quantity: if left != 0: # partial collect entity.quantity = left else: packet = make_packet("collect", eid=entity.eid, destination=self.player.eid) packet += make_packet("destroy", eid=entity.eid) self.factory.broadcast(packet) self.factory.destroy_entity(entity) packet = self.inventory.save_to_packet() self.transport.write(packet) def entities_near(self, radius): """ Obtain the entities within a radius of this player. Radius is measured in blocks. """ chunk_radius = int(radius // 16 + 1) chunkx, chaff, chunkz, chaff = split_coords(self.location.pos.x, self.location.pos.z) minx = chunkx - chunk_radius maxx = chunkx + chunk_radius + 1 minz = chunkz - chunk_radius maxz = chunkz + chunk_radius + 1 for x, z in product(xrange(minx, maxx), xrange(minz, maxz)): if (x, z) not in self.chunks: continue chunk = self.chunks[x, z] yieldables = [ entity for entity in chunk.entities if self.location.distance(entity.location) <= (radius * 32) ] for i in yieldables: yield i def login(self, container): """ Handle a login packet. This method wraps a login hook which is permitted to do just about anything, as long as it's asynchronous. The hook returns a ``Deferred``, which is chained to authenticate the user or disconnect them depending on the results of the authentication. """ # Check the username. If it's "Player", then the client is almost # certainly cracked, so we'll need to give them a better username. # Thankfully, there's a utility function for finding better usernames. username = container.username if username in self.factory.protocols: for name in username_alternatives(username): if name not in self.factory.protocols: container.username = name break else: self.error("Your username is already taken.") return if container.protocol < SUPPORTED_PROTOCOL: # Kick old clients. self.error("This server doesn't support your ancient client.") return elif container.protocol > SUPPORTED_PROTOCOL: # Kick new clients. self.error("This server doesn't support your newfangled client.") return log.msg("Authenticating client, protocol version %d" % container.protocol) d = self.factory.login_hook(self, container) d.addErrback(lambda *args, **kwargs: self.transport.loseConnection()) d.addCallback(lambda *args, **kwargs: self.authenticated()) def handshake(self, container): if not self.factory.handshake_hook(self, container): self.transport.loseConnection() def chat(self, container): if container.message.startswith("/"): pp = {"factory": self.factory} commands = retrieve_plugins(IChatCommand, parameters=pp) # Register aliases. for plugin in commands.values(): for alias in plugin.aliases: commands[alias] = plugin params = container.message[1:].split(" ") command = params.pop(0).lower() if command and command in commands: def cb(iterable): for line in iterable: self.write_packet("chat", message=line) def eb(error): self.write_packet("chat", message="Error: %s" % error.getErrorMessage()) d = maybeDeferred(commands[command].chat_command, self.username, params) d.addCallback(cb) d.addErrback(eb) else: self.write_packet("chat", message="Unknown command: %s" % command) else: # Send the message up to the factory to be chatified. message = "<%s> %s" % (self.username, container.message) self.factory.chat(message) def use(self, container): """ For each entity in proximity (4 blocks), check if it is the target of this packet and call all hooks that stated interested in this type. """ nearby_players = self.factory.players_near(self.player, 4) for entity in chain(self.entities_near(4), nearby_players): if entity.eid == container.target: for hook in self.use_hooks[entity.name]: hook.use_hook(self.factory, self.player, entity, container.button == 0) break @inlineCallbacks def digging(self, container): if container.x == -1 and container.z == -1 and container.y == 255: # Lala-land dig packet. Discard it for now. return # Player drops currently holding item/block. if (container.state == "dropped" and container.face == "-y" and container.x == 0 and container.y == 0 and container.z == 0): i = self.player.inventory holding = i.holdables[self.player.equipped] if holding: primary, secondary, count = holding if i.consume((primary, secondary), self.player.equipped): dest = self.location.in_front_of(2) coords = dest.pos._replace(y=dest.pos.y + 1) self.factory.give(coords, (primary, secondary), 1) # Re-send inventory. packet = self.inventory.save_to_packet() self.transport.write(packet) # If no items in this slot are left, this player isn't # holding an item anymore. if i.holdables[self.player.equipped] is None: packet = make_packet("entity-equipment", eid=self.player.eid, slot=0, primary=65535, secondary=0) self.factory.broadcast_for_others(packet, self) return if container.state == "shooting": self.shoot_arrow() return bigx, smallx, bigz, smallz = split_coords(container.x, container.z) coords = smallx, container.y, smallz try: chunk = self.chunks[bigx, bigz] except KeyError: self.error("Couldn't dig in chunk (%d, %d)!" % (bigx, bigz)) return block = chunk.get_block((smallx, container.y, smallz)) if container.state == "started": # Run pre dig hooks for hook in self.pre_dig_hooks: cancel = yield maybeDeferred( hook.pre_dig_hook, self.player, (container.x, container.y, container.z), block) if cancel: return tool = self.player.inventory.holdables[self.player.equipped] # Check to see whether we should break this block. if self.dig_policy.is_1ko(block, tool): self.run_dig_hooks(chunk, coords, blocks[block]) else: # Set up a timer for breaking the block later. dtime = time() + self.dig_policy.dig_time(block, tool) self.last_dig = coords, block, dtime elif container.state == "stopped": # The client thinks it has broken a block. We shall see. if not self.last_dig: return oldcoords, oldblock, dtime = self.last_dig if oldcoords != coords or oldblock != block: # Nope! self.last_dig = None return dtime -= time() # When enough time has elapsed, run the dig hooks. d = deferLater(reactor, max(dtime, 0), self.run_dig_hooks, chunk, coords, blocks[block]) d.addCallback(lambda none: setattr(self, "last_dig", None)) def run_dig_hooks(self, chunk, coords, block): """ Destroy a block and run the post-destroy dig hooks. """ x, y, z = coords l = [] for hook in self.dig_hooks: l.append(maybeDeferred(hook.dig_hook, chunk, x, y, z, block)) if block.breakable: chunk.destroy(coords) dl = DeferredList(l) dl.addCallback(lambda none: self.factory.flush_chunk(chunk)) @inlineCallbacks def build(self, container): if container.x == -1 and container.z == -1 and container.y == 255: # Lala-land build packet. Discard it for now. return # Is the target being selected? bigx, smallx, bigz, smallz = split_coords(container.x, container.z) try: chunk = self.chunks[bigx, bigz] except KeyError: self.error("Couldn't select in chunk (%d, %d)!" % (bigx, bigz)) return # Try to open it first for hook in self.open_hooks: window = yield maybeDeferred( hook.open_hook, self, container, chunk.get_block((smallx, container.y, smallz))) if window: self.write_packet("window-open", wid=window.wid, type=window.identifier, title=window.title, slots=window.slots_num) packet = window.save_to_packet() self.transport.write(packet) # window opened return # Ignore clients that think -1 is placeable. if container.primary == -1: return # Special case when face is "noop": Update the status of the currently # held block rather than placing a new block. if container.face == "noop": return if container.primary in blocks: block = blocks[container.primary] elif container.primary in items: block = items[container.primary] else: log.err("Ignoring request to place unknown block %d" % container.primary) return # it's the top of the world, you can't build here if container.y == 127 and container.face == '+y': return # Run pre-build hooks. These hooks are able to interrupt the build # process. builddata = BuildData(block, 0x0, container.x, container.y, container.z, container.face) for hook in self.pre_build_hooks: cont, builddata, cancel = yield maybeDeferred( hook.pre_build_hook, self.player, builddata) if cancel: # Flush damaged chunks. for chunk in self.chunks.itervalues(): self.factory.flush_chunk(chunk) return if not cont: break # Run the build. try: yield maybeDeferred(self.run_build, builddata) except BuildError: return newblock = builddata.block.slot coords = adjust_coords_for_face( (builddata.x, builddata.y, builddata.z), builddata.face) # Run post-build hooks. These are merely callbacks which cannot # interfere with the build process, largely because the build process # already happened. for hook in self.post_build_hooks: yield maybeDeferred(hook.post_build_hook, self.player, coords, builddata.block) # Feed automatons. for automaton in self.factory.automatons: if newblock in automaton.blocks: automaton.feed(coords) # Re-send inventory. # XXX this could be optimized if/when inventories track damage. packet = self.inventory.save_to_packet() self.transport.write(packet) # Flush damaged chunks. for chunk in self.chunks.itervalues(): self.factory.flush_chunk(chunk) def run_build(self, builddata): block, metadata, x, y, z, face = builddata # Don't place items as blocks. if block.slot not in blocks: raise BuildError("Couldn't build item %r as block" % block) # Check for orientable blocks. if not metadata and block.orientable(): metadata = block.orientation(face) if metadata is None: # Oh, I guess we can't even place the block on this face. raise BuildError("Couldn't orient block %r on face %s" % (block, face)) # Make sure we can remove it from the inventory first. if not self.player.inventory.consume( (block.slot, 0), self.player.equipped): # Okay, first one was a bust; maybe we can consume the related # block for dropping instead? if not self.player.inventory.consume(block.drop, self.player.equipped): raise BuildError("Couldn't consume %r from inventory" % block) # Offset coords according to face. x, y, z = adjust_coords_for_face((x, y, z), face) # Set the block and data. dl = [self.factory.world.set_block((x, y, z), block.slot)] if metadata: dl.append(self.factory.world.set_metadata((x, y, z), metadata)) return DeferredList(dl) def equip(self, container): self.player.equipped = container.item # Inform everyone about the item the player is holding now. item = self.player.inventory.holdables[self.player.equipped] if item is None: # Empty slot. Use signed short -1 == unsigned 65535. primary, secondary = 65535, 0 else: primary, secondary, count = item packet = make_packet("entity-equipment", eid=self.player.eid, slot=0, primary=primary, secondary=secondary) self.factory.broadcast_for_others(packet, self) def pickup(self, container): self.factory.give((container.x, container.y, container.z), (container.primary, container.secondary), container.count) def animate(self, container): # Broadcast the animation of the entity to everyone else. Only swing # arm is send by notchian clients. packet = make_packet("animate", eid=self.player.eid, animation=container.animation) self.factory.broadcast_for_others(packet, self) @inlineCallbacks def wclose(self, container): # run all hooks for hook in self.close_hooks: yield maybeDeferred(hook.close_hook, self, container) @inlineCallbacks def waction(self, container): # run hooks until handled handled = False for hook in self.click_hooks: res = yield maybeDeferred(hook.click_hook, self, container) handled = handled or res self.write_packet("window-token", wid=container.wid, token=container.token, acknowledged=handled) def wcreative(self, container): # apply inventory change that was done in creative mode applied = self.inventory.creative(container.slot, container.primary, container.secondary, container.quantity) if applied: # Inform other players about changes to this player's equipment. equipped_slot = self.player.equipped + 36 if container.slot == equipped_slot: packet = make_packet( "entity-equipment", eid=self.player.eid, # XXX why 0? why not the actual slot? slot=0, primary=container.primary, secondary=container.secondary, ) self.factory.broadcast_for_others(packet, self) def shoot_arrow(self): # TODO 1. Create arrow entity: arrow = Arrow(self.factory, self.player) # 2. Register within the factory: self.factory.register_entity(arrow) # 3. Run it: arrow.run() pass def sign(self, container): bigx, smallx, bigz, smallz = split_coords(container.x, container.z) try: chunk = self.chunks[bigx, bigz] except KeyError: self.error("Couldn't handle sign in chunk (%d, %d)!" % (bigx, bigz)) return if (smallx, container.y, smallz) in chunk.tiles: new = False s = chunk.tiles[smallx, container.y, smallz] else: new = True s = Sign(smallx, container.y, smallz) chunk.tiles[smallx, container.y, smallz] = s s.text1 = container.line1 s.text2 = container.line2 s.text3 = container.line3 s.text4 = container.line4 chunk.dirty = True # The best part of a sign isn't making one, it's showing everybody # else on the server that you did. packet = make_packet("sign", container) self.factory.broadcast_for_chunk(packet, bigx, bigz) # Run sign hooks. for hook in self.sign_hooks: hook.sign_hook(self.factory, chunk, container.x, container.y, container.z, [s.text1, s.text2, s.text3, s.text4], new) def disable_chunk(self, x, z): key = x, z log.msg("Disabling chunk %d, %d" % key) if key not in self.chunks: log.msg("...But the chunk wasn't loaded!") return # Remove the chunk from cache. chunk = self.chunks.pop(key) for entity in chunk.entities: self.write_packet("destroy", eid=entity.eid) self.write_packet("prechunk", x=x, z=z, enabled=0) def enable_chunk(self, x, z): """ Request a chunk. This function will asynchronously obtain the chunk, and send it on the wire. :returns: `Deferred` that will be fired when the chunk is obtained, with no arguments """ log.msg("Enabling chunk %d, %d" % (x, z)) if (x, z) in self.chunks: log.msg("...But the chunk was already loaded!") return succeed(None) d = self.factory.world.request_chunk(x, z) @d.addCallback def cb(chunk): self.chunks[x, z] = chunk return chunk d.addCallback(self.send_chunk) return d def send_chunk(self, chunk): log.msg("Sending chunk %d, %d" % (chunk.x, chunk.z)) self.write_packet("prechunk", x=chunk.x, z=chunk.z, enabled=1) packet = chunk.save_to_packet() self.transport.write(packet) for entity in chunk.entities: packet = entity.save_to_packet() self.transport.write(packet) for entity in chunk.tiles.itervalues(): if entity.name == "Sign": packet = entity.save_to_packet() self.transport.write(packet) def send_initial_chunk_and_location(self): """ Send the initial chunks and location. This method sends more than one chunk; since Beta 1.2, it must send nearly fifty chunks before the location can be safely sent. """ # Disable located hooks. We'll re-enable them at the end. self.state = STATE_AUTHENTICATED log.msg("Initial, position %d, %d, %d" % self.location.pos) x, y, z = self.location.pos.to_block() bigx, smallx, bigz, smallz = split_coords(x, z) # Spawn the 49 chunks in a square around the spawn, *before* spawning # the player. Otherwise, there's a funky Beta 1.2 bug which causes the # player to not be able to move. d = gatherResults([ self.enable_chunk(i, j) for i, j in product(xrange(bigx - 3, bigx + 3), xrange(bigz - 3, bigz + 3)) ]) # What to do if we can't load a given chunk? Just kick 'em. d.addErrback(lambda fail: self.error("Couldn't load a chunk... :c")) # Don't dare send more chunks beyond the initial one until we've # spawned. Once we've spawned, set our status to LOCATED and then # update_location() will work. @d.addCallback def located(none): self.state = STATE_LOCATED # Ensure that we're above-ground. self.ascend(0) d.addCallback(lambda none: self.update_location()) d.addCallback(lambda none: self.position_changed()) # Send the MOTD. if self.motd: @d.addCallback def motd(none): self.write_packet("chat", message=self.motd.replace( "<tagline>", get_motd())) # Finally, start the secondary chunk loop. d.addCallback(lambda none: self.update_chunks()) def update_chunks(self): x, y, z = self.location.pos.to_block() x, chaff, z, chaff = split_coords(x, z) new = set((i + x, j + z) for i, j in circle) old = set(self.chunks.iterkeys()) added = new - old discarded = old - new # Perhaps some explanation is in order. # The cooperate() function iterates over the iterable it is fed, # without tying up the reactor, by yielding after each iteration. The # inner part of the generator expression generates all of the chunks # around the currently needed chunk, and it sorts them by distance to # the current chunk. The end result is that we load chunks one-by-one, # nearest to furthest, without stalling other clients. if self.chunk_tasks: for task in self.chunk_tasks: try: task.stop() except (TaskDone, TaskFailed): pass self.chunk_tasks = [ cooperate( self.enable_chunk(i, j) for i, j in sorted( added, key=lambda t: (t[0] - x)**2 + (t[1] - z)**2)), cooperate(self.disable_chunk(i, j) for i, j in discarded) ] def update_time(self): self.write_packet("time", timestamp=int(self.factory.time)) def connectionLost(self, reason): """ Cleanup after a lost connection. Most of the time, these connections are lost cleanly; we don't have any cleanup to do in the unclean case since clients don't have any kind of pending state which must be recovered. Remember, the connection can be lost before identification and authentication, so ``self.username`` and ``self.player`` can be None. """ if self.username and self.player: self.factory.world.save_player(self.username, self.player) if self.player: self.factory.destroy_entity(self.player) packet = make_packet("destroy", eid=self.player.eid) self.factory.broadcast(packet) if self.username: packet = make_packet("players", name=self.username, online=False, ping=0) self.factory.broadcast(packet) self.factory.chat("%s has left the game." % self.username) self.factory.teardown_protocol(self) # We are now torn down. After this point, there will be no more # factory stuff, just our own personal stuff. del self.factory if self.time_loop: self.time_loop.stop() if self.chunk_tasks: for task in self.chunk_tasks: try: task.stop() except (TaskDone, TaskFailed): pass
class TestWindowIntegration(unittest.TestCase): def setUp(self): self.i = InventoryWindow(Inventory()) def test_craft_wood_from_log(self): self.i.inventory.add(bravo.blocks.blocks["log"].key, 1) # Select log from holdables. self.i.select(36) self.assertEqual(self.i.selected, (bravo.blocks.blocks["log"].slot, 0, 1)) # Select log into crafting. self.i.select(1) self.assertEqual(self.i.slots.crafting[0], (bravo.blocks.blocks["log"].slot, 0, 1)) self.assertTrue(self.i.slots.recipe) self.assertEqual(self.i.slots.crafted[0], (bravo.blocks.blocks["wood"].slot, 0, 4)) # Select wood from crafted. self.i.select(0) self.assertEqual(self.i.selected, (bravo.blocks.blocks["wood"].slot, 0, 4)) self.assertEqual(self.i.slots.crafting[0], None) self.assertEqual(self.i.slots.crafted[0], None) # And select wood into holdables. self.i.select(36) self.assertEqual(self.i.selected, None) self.assertEqual(self.i.inventory.holdables[0], (bravo.blocks.blocks["wood"].slot, 0, 4)) self.assertEqual(self.i.slots.crafting[0], None) self.assertEqual(self.i.slots.crafted[0], None) def test_craft_torches(self): self.i.inventory.add(bravo.blocks.items["coal"].key, 2) self.i.inventory.add(bravo.blocks.items["stick"].key, 2) # Select coal from holdables. self.i.select(36) self.assertEqual(self.i.selected, (bravo.blocks.items["coal"].slot, 0, 2)) # Select coal into crafting. self.i.select(1) self.assertEqual(self.i.slots.crafting[0], (bravo.blocks.items["coal"].slot, 0, 2)) # Select stick from holdables. self.i.select(37) self.assertEqual(self.i.selected, (bravo.blocks.items["stick"].slot, 0, 2)) # Select stick into crafting. self.i.select(3) self.assertEqual(self.i.slots.crafting[2], (bravo.blocks.items["stick"].slot, 0, 2)) self.assertTrue(self.i.slots.recipe) self.assertEqual(self.i.slots.crafted[0], (bravo.blocks.blocks["torch"].slot, 0, 4)) # Select torches from crafted. self.i.select(0) self.assertEqual(self.i.selected, (bravo.blocks.blocks["torch"].slot, 0, 4)) self.i.select(0) self.assertEqual(self.i.selected, (bravo.blocks.blocks["torch"].slot, 0, 8)) self.assertEqual(self.i.slots.crafting[0], None) self.assertEqual(self.i.slots.crafted[0], None) # And select torches into holdables. self.i.select(36) self.assertEqual(self.i.selected, None) self.assertEqual(self.i.inventory.holdables[0], (bravo.blocks.blocks["torch"].slot, 0, 8)) self.assertEqual(self.i.slots.crafting[0], None) self.assertEqual(self.i.slots.crafted[0], None) def test_armor_slots_take_one_item_only(self): self.i.inventory.add((bravo.blocks.items["iron-helmet"].slot, 0), 5) self.i.select(36) self.i.select(5) self.assertEqual(self.i.inventory.armor[0], (bravo.blocks.items["iron-helmet"].slot, 0, 1)) self.assertEqual(self.i.selected, (bravo.blocks.items["iron-helmet"].slot, 0, 4)) # Exchanging one iron-helmet in the armor slot against 5 gold-helmet in the hand # is not possible. self.i.inventory.add((bravo.blocks.items["gold-helmet"].slot, 0), 5) self.i.select(36) self.i.select(5) self.assertEqual(self.i.inventory.armor[0], (bravo.blocks.items["iron-helmet"].slot, 0, 1)) self.assertEqual(self.i.selected, (bravo.blocks.items["gold-helmet"].slot, 0, 5)) def test_armor_slots_take_armor_items_only(self): """ Confirm that dirt cannot be used as a helmet. This is the exact test case from #175. """ self.i.inventory.add((bravo.blocks.blocks["dirt"].slot, 0), 10) self.i.select(36) self.assertFalse(self.i.select(5)) self.assertEqual(self.i.inventory.armor[0], None) self.assertEqual(self.i.selected, (bravo.blocks.blocks["dirt"].slot, 0, 10)) def test_pumpkin_as_helmet(self): self.i.inventory.add((bravo.blocks.blocks["pumpkin"].slot, 0), 1) self.i.select(36) self.i.select(5) self.assertEqual(self.i.inventory.armor[0], (bravo.blocks.blocks["pumpkin"].slot, 0, 1)) self.assertEqual(self.i.selected, None) def test_armor_only_in_matching_slots(self): for index, item in enumerate([ "leather-helmet", "chainmail-chestplate", "diamond-leggings", "gold-boots" ]): self.i.inventory.add((bravo.blocks.items[item].slot, 0), 1) self.i.select(36) # Can't be placed in other armor slots. other_slots = list(range(4)) other_slots.remove(index) for i in other_slots: self.assertFalse(self.i.select(5 + i)) # But it can in the appropriate slot. self.assertTrue(self.i.select(5 + index)) self.assertEqual(self.i.inventory.armor[index], (bravo.blocks.items[item].slot, 0, 1)) def test_shift_click_crafted(self): # Select log into crafting. self.i.inventory.add(bravo.blocks.blocks["log"].key, 2) self.i.select(36) self.i.select(1) # Shift-Click on wood from crafted. self.i.select(0, False, True) self.assertEqual(self.i.selected, None) self.assertEqual(self.i.inventory.holdables[8], (bravo.blocks.blocks["wood"].slot, 0, 4)) # Move crafted wood to another slot self.i.select(44) self.i.select(18) # One more time self.i.select(0, False, True) self.assertEqual(self.i.selected, None) self.assertEqual(self.i.inventory.storage[9], (bravo.blocks.blocks["wood"].slot, 0, 8)) def test_shift_click_crafted_almost_full_inventory(self): # NOTE:Notchian client works this way: you lose items # that was not moved to inventory. So, it's not a bug. # there is space for 3 `wood`s only self.i.inventory.storage[:] = [Slot(1, 0, 64)] * 27 self.i.inventory.holdables[:] = [ Slot(bravo.blocks.blocks["wood"].slot, 0, 64) ] * 9 self.i.inventory.holdables[1] = Slot(bravo.blocks.blocks["wood"].slot, 0, 63) self.i.inventory.holdables[2] = Slot(bravo.blocks.blocks["wood"].slot, 0, 63) self.i.inventory.holdables[3] = Slot(bravo.blocks.blocks["wood"].slot, 0, 63) # Select log into crafting. self.i.slots.crafting[0] = Slot(bravo.blocks.blocks["log"].slot, 0, 2) self.i.slots.update_crafted() # Shift-Click on wood from crafted. self.assertTrue(self.i.select(0, False, True)) self.assertEqual(self.i.selected, None) self.assertEqual(self.i.inventory.holdables[1], (bravo.blocks.blocks["wood"].slot, 0, 64)) self.assertEqual(self.i.inventory.holdables[2], (bravo.blocks.blocks["wood"].slot, 0, 64)) self.assertEqual(self.i.inventory.holdables[3], (bravo.blocks.blocks["wood"].slot, 0, 64)) self.assertEqual(self.i.slots.crafting[0], (bravo.blocks.blocks["log"].slot, 0, 1)) self.assertEqual(self.i.slots.crafted[0], (bravo.blocks.blocks["wood"].slot, 0, 4)) def test_shift_click_crafted_full_inventory(self): # there is no space left self.i.inventory.storage[:] = [Slot(1, 0, 64)] * 27 self.i.inventory.holdables[:] = [ Slot(bravo.blocks.blocks["wood"].slot, 0, 64) ] * 9 # Select log into crafting. self.i.slots.crafting[0] = Slot(bravo.blocks.blocks["log"].slot, 0, 2) self.i.slots.update_crafted() # Shift-Click on wood from crafted. self.assertFalse(self.i.select(0, False, True)) self.assertEqual(self.i.selected, None) self.assertEqual(self.i.slots.crafting[0], (bravo.blocks.blocks["log"].slot, 0, 2)) def test_close_window(self): items, packets = self.i.close() self.assertEqual(len(items), 0) self.assertEqual(packets, "") self.i.slots.crafting[0] = Slot(bravo.blocks.items["coal"].slot, 0, 1) self.i.slots.crafting[2] = Slot(bravo.blocks.items["stick"].slot, 0, 1) self.i.inventory.storage[0] = Slot(3, 0, 1) # Force crafting table to be rechecked. self.i.slots.update_crafted() self.i.select(9) items, packets = self.i.close() self.assertEqual(self.i.selected, None) self.assertEqual(self.i.slots.crafted[0], None) self.assertEqual(self.i.slots.crafting, [None] * 4) self.assertEqual(len(items), 3) self.assertEqual(items[0], (263, 0, 1)) self.assertEqual(items[1], (280, 0, 1)) self.assertEqual(items[2], (3, 0, 1))
class TestInventoryIntegration(unittest.TestCase): def setUp(self): # like player's inventory window self.i = InventoryWindow(Inventory()) def test_internals(self): self.assertEqual( self.i.metalist, [[None], [None] * 4, [None] * 4, [None] * 27, [None] * 9]) def test_container_resolution(self): c, i = self.i.container_for_slot(0) self.assertTrue(c is self.i.slots.crafted) self.assertEqual(i, 0) c, i = self.i.container_for_slot(2) self.assertTrue(c is self.i.slots.crafting) self.assertEqual(i, 1) c, i = self.i.container_for_slot(7) self.assertTrue(c is self.i.inventory.armor) self.assertEqual(i, 2) c, i = self.i.container_for_slot(18) self.assertTrue(c is self.i.inventory.storage) self.assertEqual(i, 9) c, i = self.i.container_for_slot(44) self.assertTrue(c is self.i.inventory.holdables) self.assertEqual(i, 8) def test_slots_resolution(self): self.assertEqual(self.i.slot_for_container(self.i.slots.crafted, 0), 0) self.assertEqual(self.i.slot_for_container(self.i.slots.crafting, 1), 2) self.assertEqual(self.i.slot_for_container(self.i.slots.storage, 0), -1) self.assertEqual(self.i.slot_for_container(self.i.inventory.armor, 2), 7) self.assertEqual( self.i.slot_for_container(self.i.inventory.storage, 26), 35) self.assertEqual( self.i.slot_for_container(self.i.inventory.holdables, 0), 36) self.assertEqual(self.i.slot_for_container(self.i.slots.crafted, 2), -1) def test_load_holdables_from_list(self): l = [None] * len(self.i) l[36] = 20, 0, 1 self.i.load_from_list(l) self.assertEqual(self.i.inventory.holdables[0], (20, 0, 1)) c, i = self.i.container_for_slot(7) self.assertTrue(c is self.i.inventory.armor) c, i = self.i.container_for_slot(2) self.assertTrue(c is self.i.slots.crafting) def test_select_stack(self): self.i.inventory.holdables[0] = Slot(2, 0, 1) self.i.inventory.holdables[1] = Slot(2, 0, 1) self.i.select(37) self.i.select(36) self.assertEqual(self.i.inventory.holdables[0], (2, 0, 2)) self.assertEqual(self.i.inventory.holdables[1], None) def test_select_switch(self): self.i.inventory.holdables[0] = Slot(2, 0, 1) self.i.inventory.holdables[1] = Slot(3, 0, 1) self.i.select(36) self.i.select(37) self.i.select(36) self.assertEqual(self.i.inventory.holdables[0], (3, 0, 1)) self.assertEqual(self.i.inventory.holdables[1], (2, 0, 1)) def test_select_secondary_switch(self): self.i.inventory.holdables[0] = Slot(2, 0, 1) self.i.inventory.holdables[1] = Slot(3, 0, 1) self.i.select(36) self.i.select(37, True) self.i.select(36, True) self.assertEqual(self.i.inventory.holdables[0], (3, 0, 1)) self.assertEqual(self.i.inventory.holdables[1], (2, 0, 1)) def test_select_outside_window(self): self.assertFalse(self.i.select(64537)) def test_select_secondary(self): self.i.inventory.holdables[0] = Slot(2, 0, 4) self.i.select(36, True) self.assertEqual(self.i.inventory.holdables[0], (2, 0, 2)) self.assertEqual(self.i.selected, (2, 0, 2)) def test_select_secondary_empty(self): for i in range(0, 45): self.assertFalse(self.i.select(i, True)) def test_select_secondary_outside_window(self): """ Test that outrageous selections, such as those generated by clicking outside inventory windows, fail cleanly. """ self.assertFalse(self.i.select(64537), True) def test_select_secondary_selected(self): self.i.selected = Slot(2, 0, 2) self.i.select(36, True) self.assertEqual(self.i.inventory.holdables[0], (2, 0, 1)) self.assertEqual(self.i.selected, (2, 0, 1)) def test_select_secondary_odd(self): self.i.inventory.holdables[0] = Slot(2, 0, 3) self.i.select(36, True) self.assertEqual(self.i.inventory.holdables[0], (2, 0, 1)) self.assertEqual(self.i.selected, (2, 0, 2)) def test_select_fill_up_stack(self): # create two stacks self.i.inventory.holdables[0] = Slot(2, 0, 40) self.i.inventory.holdables[1] = Slot(2, 0, 30) # select first one self.i.select(36) # first slot is now empty - holding 40 items self.assertEqual(self.i.selected, (2, 0, 40)) # second stack is untouched self.assertEqual(self.i.inventory.holdables[1], (2, 0, 30)) # select second stack with left click self.i.select(37) # sums up to more than 64 items - fill up the second stack self.assertEqual(self.i.inventory.holdables[1], (2, 0, 64)) # still hold the left overs self.assertEqual(self.i.selected, (2, 0, 6)) def test_select_secondary_fill_up_stack(self): # create two stacks self.i.inventory.holdables[0] = Slot(2, 0, 40) self.i.inventory.holdables[1] = Slot(2, 0, 30) # select first one self.i.select(36) # first slot is now empty - holding 40 items self.assertEqual(self.i.selected, (2, 0, 40)) # second stack is untouched self.assertEqual(self.i.inventory.holdables[1], (2, 0, 30)) # select second stack with right click self.i.select(37, True) # sums up to more than 64 items self.assertEqual(self.i.inventory.holdables[1], (2, 0, 31)) # still hold the left overs self.assertEqual(self.i.selected, (2, 0, 39)) def test_stacking_items(self): # setup initial items self.i.slots.crafting[0] = Slot(1, 0, 2) self.i.inventory.storage[0] = Slot(2, 0, 1) self.i.inventory.storage[2] = Slot(1, 0, 3) self.i.inventory.holdables[0] = Slot(3, 0, 1) self.i.inventory.holdables[2] = Slot(1, 0, 62) self.i.inventory.holdables[4] = Slot(1, 0, 4) # shift-LMB on crafting area self.i.select(1, False, True) self.assertEqual(self.i.slots.crafting[0], None) self.assertEqual(self.i.inventory.storage[1], None) self.assertEqual(self.i.inventory.storage[2], (1, 0, 5)) # shift-LMB on storage area self.i.select(11, False, True) self.assertEqual(self.i.inventory.storage[2], None) self.assertEqual(self.i.inventory.holdables[2], (1, 0, 64)) self.assertEqual(self.i.inventory.holdables[4], (1, 0, 7)) # shift-RMB on holdables area self.i.select(38, True, True) self.assertEqual(self.i.inventory.holdables[2], None) self.assertEqual(self.i.inventory.storage[1], (1, 0, 64)) # check if item goes from crafting area directly to # holdables if possible self.i.slots.crafting[1] = Slot(1, 0, 60) self.i.inventory.storage[3] = Slot(1, 0, 63) self.i.select(2, True, True) self.assertEqual(self.i.slots.crafting[1], None) self.assertEqual(self.i.inventory.storage[2], (1, 0, 2)) self.assertEqual(self.i.inventory.storage[3], (1, 0, 64)) self.assertEqual(self.i.inventory.holdables[4], (1, 0, 64)) def test_unstackable_items(self): shovel = (bravo.blocks.items["wooden-shovel"].slot, 0, 1) self.i.inventory.storage[0] = Slot(*shovel) self.i.inventory.storage[1] = Slot(*shovel) self.i.select(9) self.i.select(10) self.assertEqual(self.i.inventory.storage[0], None) self.assertEqual(self.i.inventory.storage[1], shovel) self.assertEqual(self.i.selected, shovel) self.i.select(36) self.i.select(10, False, True) self.assertEqual(self.i.inventory.holdables[0], shovel) self.assertEqual(self.i.inventory.holdables[1], shovel) def test_drop_selected_all(self): self.i.selected = Slot(1, 0, 3) items = self.i.drop_selected() self.assertEqual(self.i.selected, None) self.assertEqual(items, [(1, 0, 3)]) def test_drop_selected_one(self): self.i.selected = Slot(1, 0, 3) items = self.i.drop_selected(True) self.assertEqual(self.i.selected, (1, 0, 2)) self.assertEqual(items, [(1, 0, 1)])