Source code for

from itertools import chain, izip
from construct import Container

from bravo import blocks
from bravo.beta.packets import make_packet
from bravo.beta.structures import Slot
from bravo.inventory import SerializableSlots
from bravo.inventory.slots import Crafting, Workbench, LargeChestStorage

[docs]class Window(SerializableSlots): """ Item manager The ``Window`` covers all kinds of inventory and crafting windows, ranging from user inventories to furnaces and workbenches. The ``Window`` agregates player's inventory and other crafting/storage slots as building blocks of the window. :param int wid: window ID :param Inventory inventory: player's inventory object :param SlotsSet slots: other window slots """ def __init__(self, wid, inventory, slots): self.inventory = inventory self.slots = slots self.wid = wid self.selected = None self.coords = None # NOTE: The property must be defined in every final class # of certain window. Never use generic one. This can lead to # awfull bugs. #@property #def metalist(self): # m = [self.slots.crafted, self.slots.crafting, # self.slots.fuel,] # m += [, self.inventory.holdables] # return m @property def slots_num(self): return self.slots.slots_num @property def identifier(self): return self.slots.identifier @property def title(self): return self.slots.title
[docs] def container_for_slot(self, slot): """ Retrieve the table and index for a given slot. There is an isomorphism here which allows all of the tables of this ``Window`` to be viewed as a single large table of slots. """ for l in self.metalist: if not len(l): continue if slot < len(l): return l, slot slot -= len(l)
[docs] def slot_for_container(self, table, index): """ Retrieve slot number for given table and index. """ i = 0 for t in self.metalist: l = len(t) if t is table: if l == 0 or l <= index: return -1 else: i += index return i else: i += l return -1
[docs] def load_from_packet(self, container): """ Load data from a packet container. """ items = [None] * self.metalength for i, item in enumerate(container.items): if < 0: items[i] = None else: items[i] = Slot(, item.damage, item.count) self.load_from_list(items)
def save_to_packet(self): l = [] for item in chain(*self.metalist): if item is None: l.append(Container(primary=-1)) else: l.append(Container(primary=item.primary, secondary=item.secondary, count=item.quantity)) packet = make_packet("inventory", wid=self.wid, length=len(l), items=l) return packet
[docs] def select_stack(self, container, index): """ Handle stacking of items (Shift + RMB/LMB) """ item = container[index] if item is None: return False loop_over = enumerate # default enumerator - from start to end # same as enumerate() but in reverse order reverse_enumerate = lambda l: izip(xrange(len(l)-1, -1, -1), reversed(l)) if container is self.slots.crafting or container is self.slots.fuel: targets =, self.inventory.holdables elif container is self.slots.crafted or container is targets = self.inventory.holdables, # in this case notchian client enumerates from the end. o_O loop_over = reverse_enumerate elif container is if targets =, else: targets = self.inventory.holdables, elif container is self.inventory.holdables: if targets =, else: targets =, else: return False initial_quantity = item_quantity = item.quantity # find same item to stack for stash in targets: for i, slot in loop_over(stash): if slot is not None and slot.holds(item) and slot.quantity < 64 \ and slot.primary not in blocks.unstackable: count = slot.quantity + item_quantity if count > 64: count, item_quantity = 64, count - 64 else: item_quantity = 0 stash[i] = slot.replace(quantity=count) container[index] = item.replace(quantity=item_quantity) self.mark_dirty(stash, i) self.mark_dirty(container, index) if item_quantity == 0: container[index] = None return True # find empty space to move for stash in targets: for i, slot in loop_over(stash): if slot is None: # XXX bug; might overflow a slot! stash[i] = item.replace(quantity=item_quantity) container[index] = None self.mark_dirty(stash, i) self.mark_dirty(container, index) return True return initial_quantity != item_quantity
[docs] def select(self, slot, alternate=False, shift=False): """ Handle a slot selection. This method implements the basic public interface for interacting with ``Inventory`` objects. It is directly equivalent to mouse clicks made upon slots. :param int slot: which slot was selected :param bool alternate: whether the selection is alternate; e.g., if it was done with a right-click :param bool shift: whether the shift key is toogled """ # Look up the container and offset. # If, for any reason, our slot is out-of-bounds, then # container_for_slot will return None. In that case, catch the error # and return False. try: l, index = self.container_for_slot(slot) except TypeError: return False if l is self.inventory.armor: result, self.selected = self.inventory.select_armor(index, alternate, shift, self.selected) return result elif l is self.slots.crafted: if shift: # shift-click on crafted slot # Notchian client works this way: you lose items # that was not moved to inventory. So, it's not a bug. if (self.select_stack(self.slots.crafted, 0)): # As select_stack() call took items from crafted[0] # we must update the recipe to generate new item there self.slots.update_crafted() # and now we emulate taking of the items result, temp = self.slots.select_crafted(0, alternate, True, None) else: result = False else: result, self.selected = self.slots.select_crafted(index, alternate, shift, self.selected) return result elif shift: return self.select_stack(l, index) elif self.selected is not None and l[index] is not None: sslot = self.selected islot = l[index] if islot.holds(sslot) and islot.primary not in blocks.unstackable: # both contain the same item if alternate: if islot.quantity < 64: l[index] = islot.increment() self.selected = sslot.decrement() self.mark_dirty(l, index) else: if sslot.quantity + islot.quantity <= 64: # Sum of items fits in one slot, so this is easy. l[index] = islot.increment(sslot.quantity) self.selected = None else: # fill up slot to 64, move left overs to selection # valid for left and right mouse click l[index] = islot.replace(quantity=64) self.selected = sslot.replace( quantity=sslot.quantity + islot.quantity - 64) self.mark_dirty(l, index) else: # Default case: just swap # valid for left and right mouse click self.selected, l[index] = l[index], self.selected self.mark_dirty(l, index) else: if alternate: if self.selected is not None: sslot = self.selected l[index] = sslot.replace(quantity=1) self.selected = sslot.decrement() self.mark_dirty(l, index) elif l[index] is None: # Right click on empty inventory slot does nothing return False else: # Logically, l[index] is not None, but self.selected is. islot = l[index] scount = islot.quantity // 2 scount, lcount = islot.quantity - scount, scount l[index] = islot.replace(quantity=lcount) self.selected = islot.replace(quantity=scount) self.mark_dirty(l, index) else: # Default case: just swap. self.selected, l[index] = l[index], self.selected self.mark_dirty(l, index) # At this point, we've already finished touching our selection; this # is just a state update. if l is self.slots.crafting: self.slots.update_crafted() return True
[docs] def close(self): ''' Clear crafting areas and return items to drop and packets to send to client ''' items = [] packets = "" # slots on close action it, pk = self.slots.close(self.wid) items += it packets += pk # drop 'item on cursor' items += self.drop_selected() return items, packets
def drop_selected(self, alternate=False): items = [] if self.selected is not None: if alternate: # drop one item i = Slot(self.selected.primary, self.selected.secondary, 1) items.append(i) self.selected = self.selected.decrement() else: # drop all items.append(self.selected) self.selected = None return items def mark_dirty(self, table, index): # override later in SharedWindow pass def packets_for_dirty(self, a): # override later in SharedWindow return ""
[docs]class InventoryWindow(Window): ''' Special case of window - player's inventory window ''' def __init__(self, inventory): Window.__init__(self, 0, inventory, Crafting()) @property def slots_num(self): # Actually it doesn't matter. Client never notifies when it opens inventory return 5 @property def identifier(self): # Actually it doesn't matter. Client never notifies when it opens inventory return "inventory" @property def title(self): # Actually it doesn't matter. Client never notifies when it opens inventory return "Inventory" @property def metalist(self): m = [self.slots.crafted, self.slots.crafting] m += [self.inventory.armor,, self.inventory.holdables] return m
[docs] def creative(self, slot, primary, secondary, quantity): ''' Process inventory changes made in creative mode ''' try: container, index = self.container_for_slot(slot) except TypeError: return False # Current notchian implementation has only holdable slots. # Prevent changes in other slots. if container is self.inventory.holdables: container[index] = Slot(primary, secondary, quantity) return True else: return False
class WorkbenchWindow(Window): def __init__(self, wid, inventory): Window.__init__(self, wid, inventory, Workbench()) @property def metalist(self): # Window.metalist will work fine as well, # but this verion works a little bit faster m = [self.slots.crafted, self.slots.crafting] m += [, self.inventory.holdables] return m
[docs]class SharedWindow(Window): """ Base class for all windows with shared containers (like chests, furnace and dispenser) """ def __init__(self, wid, inventory, slots, coords): """ :param int wid: window ID :param Inventory inventory: player's inventory object :param Tile tile: tile object :param tuple coords: world coords of the tile (bigx, smallx, bigz, smallz, y) """ Window.__init__(self, wid, inventory, slots) self.coords = coords self.dirty_slots = {} # { slot : value, ... } def mark_dirty(self, table, index): # player's inventory are not shareable slots, skip it if table in self.slots.metalist: slot = self.slot_for_container(table, index) self.dirty_slots[slot] = table[index]
[docs] def packets_for_dirty(self, dirty_slots): """ Generate update packets for dirty usually privided by another window (sic!) """ packets = "" for slot, item in dirty_slots.iteritems(): if item is None: packets += make_packet("window-slot", wid=self.wid, slot=slot, primary=-1) else: packets += make_packet("window-slot", wid=self.wid, slot=slot, primary=item.primary, secondary=item.secondary, count=item.quantity) return packets
class ChestWindow(SharedWindow): @property def metalist(self): m = [,, self.inventory.holdables] return m class LargeChestWindow(SharedWindow): def __init__(self, wid, inventory, chest1, chest2, coords): chests_storage = LargeChestStorage(, SharedWindow.__init__(self, wid, inventory, chests_storage, coords) @property def metalist(self): m = [,, self.inventory.holdables] return m class FurnaceWindow(SharedWindow): @property def metalist(self): m = [self.slots.crafting, self.slots.fuel, self.slots.crafted] m += [, self.inventory.holdables] return m