Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Queryable Unified Inventory API #443

Closed
wants to merge 23 commits into from
Closed

Queryable Unified Inventory API #443

wants to merge 23 commits into from

Conversation

Mumfrey
Copy link
Member

@Mumfrey Mumfrey commented Feb 10, 2015

This pull request encapsulates a proposal for an Inventory API for Sponge. At the core it is based upon #242 by @gratimax but is revised and expanded to include the proposition outlined below. Please read all of this document before commenting on design specifics within this PR.

This is my first PR to the API side of things so be gentle.

Design Proposal for a Queryable Unified Inventory API

Many of the proposed systems for dealing with inventories in Sponge have been very close to the underlying Minecraft implementation of inventories, there are a number of drawbacks with this model:

  • Inventory topology is bifurcated into "Inventories", generally describing an actual container (the Model), and "Containers" which present a code-facing view of one or more "Inventories" (the ViewModel). Which has its own problems:

    • Generally the physical layout of the Inventory and the physical layout of the Container are not contractually linked in any meaningful way.
    • The terminology when referring to one or other of these constructs gets very wooly very quickly (especially with Containers that have a 1:1 mapping of "slots" to underlying Inventory position).

    The View in this instance (taking MVVM as our template) is a client-only user-facing object responsible for displaying the Container to the user.

  • Working with different types of inventory requires prior knowledge of the inventory's internal layout (interface is coupled invisibly to implementation) because there is no way to obtain meta information about a specific Inventory or Container. This means that:

    • consumers have to deal with the characteristics of specific inventories, and any code which wants to deal with different types of inventory must do so by handling each inventory type directly
    • This makes the API extremely brittle and not particularly extensible, since any types of inventory which are not known about in advance cannot be efficiently interacted with
    • Code which consumes the inventory API is forced to make assumptions which must then be forcibly invalidated (for example by throwing an exception) should the underlying behaviour of a particular inventory change. eg. the same brittleness applies to "built in" inventories as to mod-provided inventories over time and there is no way to deal gracefully with this.
    • In other words, assumptions have to be made, but there is no programmatic way for a consumer to validate those assumptions prior to operation
  • Because of the blurring of the distinction between Inventory and Container, functionality is cross-contaminated between the two concepts, and this lack of separation of concerns tends to (in turn) pollute any API which tries to faithfully replicate it.

Design Goals

The aim of this proposal is to address these shortcomings in the following ways:

  • Devise an API where describing an Inventory is a core aspect of interacting with an Inventory, essentially making Inventories aspect-oriented.
  • Provision for extensibility by allowing consumers to query an Inventory for the particular aspect they are interested in, and manipulate the inventory via the results of their queries.
  • Create a clear separation between Inventory and Container concepts but acknowledge their overlaps, by making Container essentially another aspect that an Inventory can have.

Starting Assumptions

As Morpheus famously said to Neo: "free your mind".

To begin talking about a new structure for Inventory interaction, it's necessary to forget everything you currently understand about Inventories. Done that? Okay, let's start with the basic concepts.

Common Inventory Operations

If we make no other assumptions about an inventory, we can essentially begin by declaring that, in many ways an Inventory exhibits the same behaviour as a Queue in that we can add items to the Inventory, consume items from the Inventory, and get the underlying capacity of the Inventory.

interface Inventory {
    /**
     * Get and remove the first available stack from this
     * Inventory
     */
    public abstract Optional<ItemStack> poll();

    /**
     * Get without removing the first available stack from this
     * Inventory
     */
    public abstract Optional<ItemStack> peek();

    /**
     * Try to put an ItemStack into this Inventory. Just like
     * Queue, this method returns true if the Inventory accepted
     * the stack and false if not, the size of the supplied
     * stack is reduced by the number of items successfully
     * consumed by the Inventory.
     */
    public abstract boolean offer(ItemStack stack);

    /**
     * The number of stacks in the Inventory
     */
    public abstract int size();

    /**
     * The maximum number of stacks the Inventory can hold
     */
    public abstract int capacity();
}

Note that our base implementations of poll and offer don't provide any position information, this is because we are not making any other assumptions about the structure of the Inventory at this point. While this might seem like an odd baseline, it begins to make sense when we consider that with a query-based Inventory API, we will be returning inventories which fall into one of the following three categories:

  • Empty Inventory - to provide for a fluent interface we don't want to return null, the Empty Inventory will be our representation of an empty result set.
  • Single-Stack Inventory - an Inventory with a single stack is effectively a Slot. By keeping the Inventory interface simple enough that a Slot can reasonably extend it, we have simple unified way of a query returning anything from an entire Inventory, to a specific set of matching slots, to a single slot containing an Item we're looking for.
  • Multi-Stack Inventory - other inventories which can contain more than a single stack.

Basis and Purpose of Queries

Before we look at how queries can work, let's first take a look at a simple use-case to understand the purpose of queries in the first place.

The PlayerInventory class actually encompasses several groups of inventory slots we may be interested in. These groups are internally (physically) separated into two arrays: the Main Inventory and the Armour Inventory. The Main Inventory comprises both the hotbar area (the 9 inventory slots always displayed on the screen) and the rest of the player's main inventory. Representing this as a basic Venn Diagram we see:

When working directly with Minecraft, the two inner inventory arrays are available directly as public members of the InventoryPlayer class. However some additional functionality is available which supports both the ViewModel-esque contract required by Containers and also some additional convenience functions for working with only the hotbar slots. Some notable drawbacks with this mixing of concerns is that the following become apparent when working with the PlayerInventory:

  • Some methods treat the slots within the inventory as a contiguous set, and treat the Armour Inventory as indices beyond the Main Inventory indices whereas other code does not, this presents an inconsistent external interface.
  • Conversely, some methods only deal with the hotbar slots, and not the rest of the Inventory
  • No distinction is made between any positions in the inventory, thus external code has to "know" the meaning of slot indices within the Inventory in advance. (A problem not confined to the InventoryPlayer class)

It should be noted that this scenario is far from unique amongst inventories, and only gets worse when dealing with Containers. For example let's take a look at a container which you're likely to be very familiar with, ContainerPlayer:

This container presents a View of 3 separate Inventories (essentially ViewModels) which in turn represent 4 distinct underlying arrays of slots (essentially the underlying Models).

You may find yourself asking "so what the hell does all this have to do with queries?" and that's a fair question. Ultimately we have 3 ways of dealing with this mess in order to improve it from the point of view of API consumers:

  1. Abstract the hell out of things. Provide "sensible" external interfaces and break everything down into small logical units which hide the underlying horribleness.
    • Pros:
      • easy to work with from a consumer's point of view
      • hides all the horrible internals
    • Cons:
      • nasty to maintain (if anything in the implementation changes then the abstraction could get more and more complex)
      • hard to extend (mod inventories and and kind of custom inventory are hard to work into this model because the abstractions themselves are "hard coded")
      • makes simple operations more complex than they need to be (consumers need to explicitly dig through layers of abstraction to get to what they want)
      • doesn't actually add any value, just hides all the nastiness
  2. Expose the underlying horribleness but provide a metadata system which assigns meaning to the exposed data structures, such as allowing a "range" of slots to be described.
    • Pros:
      • doesn't add much overhead
      • keeps people who are used to the old system happy
      • provides added value in terms of some meta information about the meanings of slots
    • Cons:
      • just as brittle as the underlying impementation
      • doesn't provide any useful abstraction
  3. (Using queries) Make metadata an intrinsic part of the API and allow consumers to stipulate exactly what part of an inventory they mean by accessing inventory via the metadata using queries:
    • Pros:
      • All the advantages of both option 1. and 2. (the underlying representations without metadata are still accessible via casting down - see implementation details below)
      • Consumers only ask for what they need, they never need to care about what type of Inventory or Container they're dealing with, only the results of a query
      • Intrinsically extensible - the nature of queries makes it easy for third-party extensibility without removing the ability to do simple instanceof-and-cast operations which would have been required before.
    • Cons:
      • May be difficult for consumers to adjust to the new model (although the nature of the implementation means that stick-in-the-muds can still use the old model if they're foolhardy enough)
      • Underlying implementation is more complex than other systems (however it should be noted that all the complex logic will be handled in the initial implementation, and that ongoing maintenance of the system will actually be simpler)

Presented with the options it's clear that option 3, using queries has merit.

Goals for a Query-based implementation

Fundamental to the concept of querying Inventories is the basic premise that:

  • An Inventory is a View of one or more "sub inventories"
  • An Inventory and its sub inventories exist in a parent-child relationship
  • An Inventory is thus a View of Views with arbitrary depth

In the InventoryPlayer example above, the relationship of the Inventory to its notional sub-inventories is as follows:

The main idea of queries is that given an unknown Inventory instance, it should be possible to query for any sub-inventory or combination of matching sub-inventories, with the query returning either all sub-inventories which match the query, or an empty set if no sub-inventories matched the query.

It's useful to bear in mind that our above definition of Inventory essentially means that everything can be represented as an Inventory right down to Slot (defined as an Inventory with only a single position) and thus a more helpful representation when considering what can be returned by a query is the following:

We're now in a position to specify what a query should actually return:

  • A Query should return all Sub Inventories of the Inventory which match the supplied criteria
  • If a sub inventory is included in the results set, its parent will not be
  • A query will never return a null result, it will return an Empty Inventory object.
  • A query will never return duplicate entries

Which produces as a consequence some assumptions for Inventory itself:

  • The result of a query is always an Inventory
  • An Inventory is Iterable<Inventory> and the returned iterator traverses the child nodes of the Inventory's hierarchy
  • An Inventory's leaf nodes should also be traversable via a method which returns an Iterator

To give some examples, based on the hierarchy above:

  • A query for an imaginary Hotbar.class should return the Hotbar inventory
  • A query for ItemTypes.TNT should return an Inventory with all of the slots containing TNT
  • A query for the imaginary InventoryRow.class should return each row in the Main Inventory and the Hotbar

Which produces the following assumptions:

  • When performing a query, if an Inventory matches the query directly, it should return itself
  • When performing a query, an Inventory should perform a depth-first search of its hierarchy. A matching child node will remove its parent from the results set if present.
  • If no children match the query, the query should return an EmptyInventory.
  • If all direct children match the query, the query should return the parent (eg. if all InventoryRow children within an InventoryGrid match the query, then the InventoryGrid is returned)

We now have enough information to begin formulating our query interface.

Inventory methods for query results

Since the result of a Query will always be an Inventory, we add some decoration and methods to our Inventory interface. Firstly, we have the Inventory extend Iterable<Inventory> as planned. Next we add a method for iterating the leaf nodes of our Inventory and a convenience function for checking whether the Inventory has no slots:

interface Inventory extends Iterable<Inventory> {
    //
    // ... code code code (see above) ...
    //

    /**
     * Returns an iterable view of all slots, use type specifier to
     * allow easy pseudo-duck-typing
     */
    public abstract <T extends Inventory> Iterable<T> slots();

    /**
     * Returns true if this Inventory contains no children
     */
    public abstract boolean isEmpty();
}

Depending on requirement, we may also wish to add some convenience methods to work with Inventorys which are result sets, for example

    /**
     * Return the first child inventory, effectively the same as
     * Inventory::iterator().next() but more convenient when we are
     * expecting a result set with only a single entry. Also use type
     * specifier to allow easy pseudo-duck-typing. If no children, then
     * returns this.
     */
    public abstract <T extends Inventory> T first();

    /**
     * Return the next sibling inventory, allows traversing the inventory
     * hierarchy without using an iterator. If no more children, returns
     * an EmptyInventory.
     */
    public abstract <T extends Inventory> T next();

Inventory methods for executing queries

It is anticipated that the full scope and possibilities of queries will only become apparent in time, and also that the initial system should be sufficiently extensible to support quite a wide scope of potential queries. However the basic query types which should be in the initial release are:

Query by type

public abstract <T extends Inventory> T query(Class<?>... types);

Query for sub-inventories matching the specified interfaces/concrete classes (effictively an instanceof check). Multiple classes can be specified and logical OR will be applied.

Example 1:

Inventory inv = ...; // An unknown inventory
PlayerInventory pinv = inv.query(PlayerInventory.class).first();

Example 2: Logical AND (querying the result of a query)

Inventory inv = ...; // An unknown inventory
HotBar hotbar = inv
    .query(InventoryRow.class)
    .query(HotBar.class).first();

Query by contents

public abstract <T extends Inventory> T query(ItemTypes... types);

Query for slots containing ItemStacks with items of the specified type. A logical OR applied between query operands.

Example:

Inventory inv = ...; // An unknown inventory
Inventory slotsWithTNT = inv.query(ItemTypes.TNT);
if (!slotsWithTNT.isEmpty()) {
    // found slots with TNT!
}

Query by property

As well as defining a (possibly multi-dimensional) hierarchy of sub-inventories, it is anticipated that some sub-inventory types will tag their members with additional data which can be used to include them in queries. For example, let's assume that we have a GridInventory interface:

/**
 * MetaInventory(?) stores arbitrary properties for child inventories
 */
interface MetaInventory extends Inventory {
    /**
     * Get a property defined in THIS inventory for the specified
     * (immediate) sub-inventory.
     */
    public abstract <T extends InventoryProperty> getProperty(Inventory child, Class<T> property);
}

/**
 * This type of inventory arranges its children in a grid.
 */
interface GridInventory extends MetaInventory {
    /**
     * Get the number of rows in the grid
     */
    public int getRows();

    /**
     * Get the number of columns in the grid
     */
    public int getColumns();

    /**
     * Get the X/Y position of the specified slot. The InventoryPos
     * class extends InventoryProperty and thus this method is just a
     * convenience method which effectively calls:
     * 
     *    this.getProperty(slot, InventoryPos.class);
     */ 
    public abstract InventoryPos getSlotPos(Inventory slot);
}

Assuming that InventoryPos extends some ficticious base class InventoryProperty, we now have a mechanism for querying by any arbitrary properties we care to define:

public abstract <T extends Inventory> T query(InventoryProperty... props);

Query for sub-inventories where the specified property is set and .equals() the supplied properties. Logical OR is applied between operands.

Example:

Inventory inv = ...; // An unknown inventory
Inventory slots = inv.query(new InventoryPos(2, 2)).first();

Query by name

Since in the real world, Inventory classes are always nameable, we can use a String overload to query by name:

public abstract <T extends Inventory> T query(String... names);

Query by arbitrary operands

To promote extensibilty, even where completely unknown Inventories are in play, we should also include a general query interface which will allow arbitrary queries to be executed:

public abstract <T extends Inventory> T query(Object... args);

Query for sub-inventories using an arbitrary check defined by the inventory class in question.

Example:

Inventory inv = ...; // An unknown inventory
SomeObject someObject = ...; // Something
Inventory result = inv.query(someObject); // ???

Multi-Dimensional Query Model

So far, for simplicity we have outlined a straightforward hierarchical model of the inventory. However it is not the intention that underlying implementations should adopt such a simple model. To facilitate querying by dimensions, we can represent underlying sub-inventories in as many different dimensions as we like, and allow queries to return corresponding views. For example we can expand our simple example above to allow the Inventory to be queried by column:

In this example the InventoryGrid contains both row and column sub-Inventories. This is by no means the limit of what can be represented by further dimensions. However note that one tree should always be considered the master view (a notional appellation), a traversal of which visits each leaf node in a deterministic order which is the iteration order as experienced by slots().

Essentially every Inventory should have a deterministic spanning tree, whose depth-first traversal will be the primary iteration order of the leaf nodes. This order is left to implementors to decide upon. Here is an example for the inventory structure shown above.

Example Spanning Tree for the Player Inventory

Example traversal of the Spanning Tree

Implications for Intended Usage

So far we have seen how queries can work in general, the main goals of incorporating queries into the API are to change the general way that consumers interact with Inventories. Without queries, a typical interaction with an Inventory might take place as follows:

  1. Consumer obtains an Inventory instance from an API object. The inventory is of a known type - eg. a PlayerInventory is obtained from a Player because it is advertised by the object.
  2. Knowing the type of inventory, the consumer performs some operations on the inventory: for example moving items from the main inventory to the hotbar, or consuming items from the inventory.

Now if the same code wishes to interact with a different type of Inventory, the code has to be adapted or rewritten, even if the other inventory has substantially the same behaviour or characteristics.

With a query-based approach, the interaction takes place as follows:

  1. Consumer obtains an Inventory instance from an API object. The inventory is not a specialised type, the return type of the getter is simply Inventory.
  2. The consumer queries for the inventory characteristics it requires, for example it queries for all InventoryRows
  3. The consumer interacts with the returned rows, or can simply skip processing if the query returns an empty set.

If the same code wishes to interact with a different type of Inventory, it can do so as long as the Inventory has InventoryRow children.

SIDE NOTE
Consider porting of old code. Old code may simply (as a stop-gap) wish to obtain the PlayerInventory and manipulate it directly, without any queries. With queries this is still supported as long as PlayerInventory is provided as an interface in the API. Consumers wishing to perform legacy interactions can either:

  1. Query directly for the InventoryPlayer interface (this works because of assumption 1 of queries - if an inventory matches a query it simply returns itself)
  2. Use an instanceof check followed by a type cast

This provides a convenient way for legacy code to be ported quickly to the new system, and transition to a full query-based implementation at the convenience of the plugin author.

Taking the intended usage into account, we can deduce some additional characteristics our API should have:

  • Any API object which has an Inventory should simply return Inventory, with no requirement to return a particular subinterface

  • Consumers should always obtain Inventory subclasses by querying for them (either directly or via instanceof check)

  • Specific Inventory subinterfaces should still be provided within the API in order to satisfy queries, however they should not generally be returned and different implementations are therefore at liberty to select whichever interfaces they wish to implement - in other words there is no hard rule that says a PlayerInventory must have InventoryRows as children for example, the query model means that this is purely optional and the only negative effect would be that consumers expecting such a structure would not be able to perform their operations.

    This works in both directions however, and while it is expected that all implementations will likely follow similar conventions (in order that queries can be relied upon across platforms), it's also possible that some implementations will be able to innovate, and extend their capabilities without in any way breaking backward compatibility.

    The implication of all this is that the shape and design of the underlying Inventories is thus free to follow a logical representation of the underlying game without at any point breaking backward compatibility.

  • This in turn leads us to the conclusion that the number of "specific" inventories should be kept to a minimum, and general-purpose Inventory interfaces should be used wherever possible. The general-purpose Inventory interfaces should essentially represent characteristics of inventories, with little regard for specific Inventory types.

Relationship with Containers

It should be clear by now that a query-based model also solves the Container problem. By acknowledging that the line between Inventory and Container can often be blurry, we can treat Containers as Inventories for the most part (since we have essentially turned Inventory into a ViewModel at this point anyway, and all a Container really is is a ViewModel) and simply have them exist as yet another type of SubInventory interface.

Features not included within this PR

Taking a longer view, the following features are worthy of consideration

  • Inventory Transactions - basically SQL transactions but for Inventories allowing commit and rollback operations
  • Aggregate Inventory Operations - basically SQL join for queries, such that a logical union of inventories can be created and subsequently queried and operated upon as if it were a single inventory. This isn't too hard to do I just don't have sufficient time to flesh it out.
  • Atomic Multi-Inventory Operations - as a poor second choice to full-blown transactional behaviour, having atomic operations (get-stack-from-first-inventory, deposit-in-second) would still be useful.
  • Cookies - congratulations, if you read this far you get a cookie (Not a real cookie, an imaginary one. What? You think I'm made of cookies or something?)

@gabizou
Copy link
Member

gabizou commented Feb 10, 2015

Cookies - congratulations, if you read this far you get a cookie (Not a real cookie, an imaginary one. What? You think I'm made of cookies or something?)

Can I have a cookie for being patient and waiting for this PR to be made?

@Mumfrey
Copy link
Member Author

Mumfrey commented Feb 10, 2015

@gabizou no you may not.

* TODO Flesh out javadoc from proposal document. For now, see proposal doc
* here: https://github.com/SpongePowered/SpongeAPI/pull/443
*/
public interface Inventory extends Iterable<Inventory>, Nameable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do all inventories really have Translatable names, even plugin-created ones?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered putting this on Container instead but in vanilla minecraft every IInventory is IWorldNameable and thus anything which is an Inventory basically has a name. I extracted that out to Nameable partly because it's a useful interface to have on its own merits but also because we could potentially graft the Nameable onto the hierarchy somewhere else if we don't like it being on the base interface.

Food for thought at any rate. Where I'm not sure about stufff I've erred on the side of simplicity and consistency with the view that we can throw it all in the bin if we feel the inclination.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't the name of the inventory be a Translatable?

@stephan-gh
Copy link
Contributor

Some small notes to the code style:

  • We don't use any redundant modifiers on interface methods to keep the interfaces clean, so we need to remove the public abstract prefixes in some of the interfaces here.
  • Some classes are missing Javadocs and some methods are missing @param and @return tags.
  • The normal Javadoc description should have an period at the end, the description of the tags shouldn't.

I've also noticed a few events in here that are already in the SpongeAPI.

/**
* Raised when an entity equips an item.
*/
public interface EntityEquipEvent extends EntityEvent, InventoryEvent {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the same as EntityEquipmentChangeEvent?

@Mumfrey
Copy link
Member Author

Mumfrey commented Feb 10, 2015

We don't use any redundant modifiers on interface methods to keep the interfaces clean, so we need to remove the public abstract prefixes in some of the interfaces here.

Okay that's fair enough, this is my first contribution to the API so I expected their would be style differences. I always use the modifiers just because it seems more readable but I can remove them no problem.

Some classes are missing Javadocs and some methods are missing @param and @return tags.

To be honest the plan was to flesh out the javadoc later, I wanted to wait until all the I's were dotted and T's were crossed but I've been too busy and @Zidane was probably going to murder me if I took any longer. Basically take a view that if there is some Javadoc, it'll get fixed later when I have two seconds to rub together, either that or someone else can beat me to it.

The normal Javadoc description should have an period at the end, the description of the tags shouldn't.

This isn't anywhere in the style guide, I'm not sure how I was supposed to know this.

I've also noticed a few events in here that are already in the SpongeAPI.

Likely because @gratimax already added them before they were added to the API. I'm sure redundant events can be removed. Any ideas which ones?

@stephan-gh
Copy link
Contributor

@Mumfrey Wasn't sure if the PR was already ready to be reviewed on code style, so I just thought I do that so we can fix them. :)

This isn't anywhere in the style guide, I'm not sure how I was supposed to know this.

Some more things are mentioned in our contribution guidelines although it's not complete. I guess you were not able to know this, but now you do. ;)

Likely because @gratimax already added them before they were added to the API. I'm sure redundant events can be removed. Any ideas which ones?

I've commented on the (possibly) duplicate events above.

@Mumfrey
Copy link
Member Author

Mumfrey commented Feb 10, 2015

@Mumfrey Wasn't sure if the PR was already ready to be reviewed on code style, so I just thought I do that so we can fix them. :)

Personally I tend to focus on the important aspects of code style (readability, wrapping, stuff having some kind of javadoc rather than none) and deal with the more fiddly parts in a second pass, at this point as long as it builds then people can review the structure of the PR and other issues can be mopped up going forward.

Some more things are mentioned in our contribution guidelines although it's not complete. I guess you were not able to know this, but now you do. ;)

I was including the contribution guidelines in my above assertion, there is no mention of full stops other than specifically mentioning they should be omitted on at clauses.

I've commented on the (possibly) duplicate events above.

I'll leave that one for @gratimax to take a view on. I've removed the public abstract modifiers and added javadoc to two classes which were missing it. The rest can wait for a second pass because there are more pressing things that need work.

@ZephireNZ
Copy link
Contributor

First off, glorious pull request.

How, in this new system, would you go about taking X amount of items from a given inventory? I imagine you would you create a query for the ItemType, but then what? Iterate over that inventory, removing item stacks until you've removed enough?

@Mumfrey
Copy link
Member Author

Mumfrey commented Feb 10, 2015

First off, glorious pull request.

How, in this new system, would you go about taking X amount of items from a given inventory? I imagine you would you create a query for the ItemType, but then what? Iterate over that inventory, removing item stacks until you've removed enough?

Yes, because remember that your result set is an Inventory. So basically if you want to consume a specific item from anywhere in the inventory you query for slots containing the inventory and then peek to get the first stack. Although now you ask it's given me an idea that maybe you could instead poll with a limit argument to allow you to consume items up to a set amount.

You need not iterate over the inventory because you can consume items directly from the query result (since it's an Inventory) so basically to consume a single item (eg to consume a arrow when you fire it).

Inventory arrows = player.getInventory().query(ItemTypes.ARROW);
if (arrows.isEmpty()) {
    // boo, no arrows
    return;
}

ItemStack quiver = arrows.peek().get(); // We know it's present so calling get() is safe
quiver.setQuantity(quiver.getQuantity() - 1); // Decrement the stack quantity

However, I think a nicer way (which I will add now you mention it) would be:

Inventory arrows = player.getInventory().query(ItemTypes.ARROW);
if (arrows.isEmpty()) {
    // boo, no arrows
    return;
}

ItemStack arrow = arrows.poll(1).get(); // Consume a stack of 1 array from the result

or even:

Optional<ItemStack> arrow = player.getInventory().query(ItemTypes.ARROW).poll(1);
if (arrow.isPresent()) {
    // blah
}

EDIT - there we go, see commit fe8375e

@Lunaphied
Copy link

This is awesome in all of its definitions, what led you to come up with a system so different?

@Mumfrey
Copy link
Member Author

Mumfrey commented Feb 10, 2015

@modwizcode there's honestly nothing really outlandish in this proposal, it's more the fact that everyone is so entrenched in the existing Inventory architecture (which, let's face it, is bloody awful) that nobody has really taken a step back and said "wait a minute, how should this work?" So much interaction with inventories to date has required far too much prior knowledge of the internals of the inventory in question, I simply tried to come up with a more expressive way of interacting with inventories.

@kitskub
Copy link
Contributor

kitskub commented Feb 10, 2015

Quick comment because I'm on my phone and can't really look through the code: instead of new InventoryPos(x, y), I think InventoryPos.of(x, y) would be cleaner.

@Mumfrey
Copy link
Member Author

Mumfrey commented Feb 10, 2015

@kitskub they're just the interfaces, how they're implemented is up to the implementation.

@kitskub
Copy link
Contributor

kitskub commented Feb 10, 2015

@Mumfrey okay. Sorry, like I said: can't read the code at the moment.

@Mumfrey
Copy link
Member Author

Mumfrey commented Feb 10, 2015

@kitskub It might be nice to have some concrete implementations in the API but I'm not sure what format that kind of thing is taking and it's up to @Zidane et al. to figure out the best way to factory these objects that's consistent with how things are done elsewhere in the API.

@boformer
Copy link
Contributor

Well done. 2 things:

  • Also add a peek(int limit) method
  • Add a way to count the number of single items contained in the inventory

And a simple question:
How can I place an item stack (e.g. a server rule book) into the first slot of the hotbar, even if the slot is filled with an existing item?

My Solution:
First, get the Inventory of the slot: playerInv.query(Hotbar.class).query(new InventoryPos(0,0)

Then offer the book item stack to the slot. If it was rejected, poll the existing item stack from the hotbar slot.

Then offer the book item stack to the hotbar slot again.

Then offer the replaced item stack to the playerInv.

If it was rejected. revert everything so that no items are lost (and drop the book).

Does it work like this?

@Mumfrey
Copy link
Member Author

Mumfrey commented Feb 10, 2015

Also add a peek(int limit) method

Hmm. Interesting. What's the use-case just for my curiosity?

Add a way to count the number of single items contained in the inventory

Okay.

And a simple question:
How can I place an item stack (e.g. a server rule book) into the first slot of the hotbar, even if the slot is filled with an existing item?

ItemStack rulebook = ...;
inventory.query(Hotbar.class).first().set(rulebook);

for other slots:

Inventory result = inventory.query(Hotbar.class);
if (!result.isEmpty()) { // check the query actually succeeded
    Hotbar hotbar = result.<Hotbar>first();
    hotbar.set(3, rulebook); // Hotbar is an OrderedInventory so we can use positional set
}

or using a fully query-based version:

inventory.query(Hotbar.class).query(new SlotIndex(3)).set(rulebook);  

@ST-DDT
Copy link
Member

ST-DDT commented Feb 10, 2015

First of all: Awesome API! (And nice and complete description with descriptive images)

@Mumfrey I guess @boformer tried to add an ItemStack to the Hotbar Slot 0. And moves any existing Item at that slot to the normal player inventory. (including later slots in the hotbar?

I have three question myself.

  • in one of the pictures you displayed above you specified the iteration order to iterate over the InventoryRows, but it is possible to iterate over InventoryColumns as well?
  • If the Inventory that is queried is a PlayerContainer as you called it (with the crafting slots), will they be included in the iteration as well?
  • How do i set the active hotbar index/slot? Hotbar.setActiveSlotIndex()

@octylFractal
Copy link
Contributor

@ST-DDT I assume it would be like this:

inventory.query(Hotbar.class).query(new SlotIndex(/* selected slot here */)).set(rulebook);  

* The maximum number of stacks the Inventory can hold. Always 1 for
* {@link Slot}s and always 0 for {@link EmptyInventory}s.
*/
int capacity();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A method for summing up all ItemStack sizes would be nice.
Inventory.query(ItemTypes.ARROW).totalCount();
Returns the total number of arrows in the inventory.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already suggested below.

@ST-DDT
Copy link
Member

ST-DDT commented Feb 10, 2015

@kenzierocks IMO this will replace the item at the given slot, removing it from the inventory, instead of moving it to another slot. (This is what i expect the method to do)

* objects in the inventory as required to accomodate the entire stack. The
* entire stack is always consumed.
*/
void set(ItemStack stack);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should return Optional<ItemStack> for cases where an other Item has been thrown out of the inventory in order to add said item. (Map.put() also returns any previous entry)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's better handled by an event imo.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

has been thrown out of the inventory

I meant the old item in the slot that has been replaced by the new item.

Using events to get the replaced item would totally break the call chain here and will most likely force me to never run this method in a situation where it replaces another item.

@me4502
Copy link
Contributor

me4502 commented Feb 17, 2015

Is this PR mergeable?

@Mumfrey Mumfrey closed this in 781a08d Feb 17, 2015
@gabizou gabizou deleted the feature/inventory branch February 21, 2015 07:51
@Zidane Zidane modified the milestone: 2.0-Release Mar 5, 2015
@oliverdunk
Copy link

So how would you backup an entirw inventory with this system? Oh, and could you make inventories serializable by default?

simon816 referenced this pull request Oct 16, 2015
- Implement EmptyInventory in the API.
- Default some Inventory methods to make implementation easier.

Signed-off-by: Chris Sanders <zidane@outlook.com>
wysohn referenced this pull request in TriggerReactor/TriggerReactor Jan 7, 2020
…@wysohn

getting it to work.  Please see the //TODO comments in InventoryEditManager for more details about what is broken.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet