Exemplo n.º 1
0
    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)
Exemplo n.º 2
0
    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)
Exemplo n.º 3
0
    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)
Exemplo n.º 4
0
    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)
Exemplo n.º 5
0
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
Exemplo n.º 6
0
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
Exemplo n.º 7
0
 def setUp(self):
     self.i = InventoryWindow(Inventory())
Exemplo n.º 8
0
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))
Exemplo n.º 9
0
 def setUp(self):
     # like player's inventory window
     self.i = InventoryWindow(Inventory())
Exemplo n.º 10
0
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)])
Exemplo n.º 11
0
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
Exemplo n.º 12
0
 def setUp(self):
     self.i = InventoryWindow(Inventory())
Exemplo n.º 13
0
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))
Exemplo n.º 14
0
 def setUp(self):
     # like player's inventory window
     self.i = InventoryWindow(Inventory())
Exemplo n.º 15
0
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)])