This pull request encapsulates a proposal for an Inventory API for Sponge. At th…e 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.
``` java
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:
![](http://eq2.co.uk/pr/inv/playerinv.png)
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`:
![](http://eq2.co.uk/pr/inv/playercontnr.png)
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:
![](http://eq2.co.uk/pr/inv/invhierarchy.png)
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:
![](http://eq2.co.uk/pr/inv/invhierarchydeep.png)
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:
``` java
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 `Inventory`s which are result sets, for example
``` java
/**
* 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
``` java
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:**
``` java
Inventory inv = ...; // An unknown inventory
PlayerInventory pinv = inv.query(PlayerInventory.class).first();
```
**Example 2: Logical AND** _(querying the result of a query)_
``` java
Inventory inv = ...; // An unknown inventory
HotBar hotbar = inv
.query(InventoryRow.class)
.query(HotBar.class).first();
```
#### Query by contents
``` java
public abstract <T extends Inventory> T query(ItemTypes... types);
```
Query for slots containing `ItemStack`s with items of the specified type. A logical `OR` applied between query operands.
**Example:**
``` java
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:
``` java
/**
* 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:
``` java
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:**
``` java
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:
``` java
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:
``` java
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:**
``` java
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:
![](http://eq2.co.uk/pr/inv/invhierarchymulti.png)
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**
![](http://eq2.co.uk/pr/inv/invhierarchyspanning.png)
**Example traversal of the Spanning Tree**
![](http://eq2.co.uk/pr/inv/invhierarchytraverse.png)
### 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 `InventoryRow`s
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 `InventoryRow`s 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?)_