Relearning the Inventory - Part 1

The inventory logic is one part of the engine with little documentation and a lot of usage. There has been at least one attempt to clean it up so why not start over an inspect the existing codebase ;)

I don’t know much from the current inventory implementation, for this reason I will start with a digging series through the codebase and try to get an overview. Feel free to follow me, I will use this post to track my first progress and also my thought process.

My starting point will be commit da8b26a2d72d0621a7eac67446281ba1a9c6c60c, just in case you want to follow in the codebase.

Entry Points

I have no starting points yet and a broad search for classes with fitting names is most of the time a good begin. Searching for Inventory gives a good initial result. Entry Points

I want to begin with the logic and not the UI. Interesting classes are marked red. There is also a testclass for the InventoryAuthoritySystem. Tests are a great documentation how a system is intended to be used and an authority system looks promising, therefore I start with the tests.

Starting with tests

The inspected class is the InventoryAuthoritySystemTest.

Lets begin with the setup logic.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    private InventoryAuthoritySystem inventoryAuthoritySystem;
private EntityRef instigator;
private EntityRef inventory;
private InventoryComponent inventoryComp;
private EntityManager entityManager;

@Before
public void setup() {
inventoryAuthoritySystem = new InventoryAuthoritySystem();
instigator = Mockito.mock(EntityRef.class);
inventory = Mockito.mock(EntityRef.class);
inventoryComp = new InventoryComponent(5);
Mockito.when(inventory.getComponent(InventoryComponent.class)).thenReturn(inventoryComp);

entityManager = Mockito.mock(EntityManager.class);
inventoryAuthoritySystem.setEntityManager(entityManager);
}

Not much to see here but we now know that an inventory is an entity with an InventoryComponent.

1. Inventory is an Entity with an InventoryComponent.

Next interesting method is the helper method to create an item.

1
2
3
4
5
6
7
8
9
10
11
    private EntityRef createItem(String stackId, int stackCount, int stackSize) {
ItemComponent itemComp = new ItemComponent();
itemComp.stackCount = (byte) stackCount;
itemComp.maxStackSize = (byte) stackSize;
itemComp.stackId = stackId;
EntityRef item = Mockito.mock(EntityRef.class);
Mockito.when(item.exists()).thenReturn(true);
Mockito.when(item.getComponent(ItemComponent.class)).thenReturn(itemComp);
Mockito.when(item.iterateComponents()).thenReturn(new LinkedList<>());
return item;
}

This gives us more information over items.

2. Item is an Entity with an ItemComponent.

3. Items can be stacks with a size limited to 127 (Byte.MaxValue).

4. Item stacks are identified by a String id.

Now we are able to understand the tests, taking testMoveItemToSlotsWithSplittingToMultipleStacks as example.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    @Test
public void testMoveItemToSlotsWithSplittingToMultipleStacks() {
int stackSize = 10;
EntityRef toInventory = inventory;
InventoryComponent toInventoryComp = toInventory.getComponent(InventoryComponent.class);
EntityRef itemA1 = createItem("A", 8, stackSize);
EntityRef itemB1 = createItem("B", 8, stackSize);
EntityRef itemA2 = createItem("A", 7, stackSize);
toInventoryComp.itemSlots.set(0, itemA1);
toInventoryComp.itemSlots.set(2, itemB1);
toInventoryComp.itemSlots.set(3, itemA2);

EntityRef fromInventory = Mockito.mock(EntityRef.class);
InventoryComponent fromInventoryComp = new InventoryComponent(5);
Mockito.when(fromInventory.getComponent(InventoryComponent.class)).thenReturn(fromInventoryComp);
EntityRef itemA3 = createItem("A", 4, stackSize);
int fromSlot = 1;
fromInventoryComp.itemSlots.set(fromSlot, itemA3);

List<Integer> toSlots = Arrays.asList(0, 1, 2, 3, 4);

// The method that gets tested:
inventoryAuthoritySystem.moveItemToSlots(instigator, fromInventory, fromSlot, toInventory, toSlots);

assertEquals(10, itemA1.getComponent(ItemComponent.class).stackCount);
assertEquals(9, itemA2.getComponent(ItemComponent.class).stackCount);
assertFalse(fromInventoryComp.itemSlots.get(fromSlot).exists());
}

The test creates a target inventory toInventory with three stacks of items (lines 3-11):

  • itemA1 is a stack with 8/10 from A at position 0
  • itemB1 is a stack with 8/10 from B at position 2
  • itemA2 is a stack with 7/10 from A at position 3

Then another inventory fromInventory is created with a stack of 4/10 A at slot 1 (line 13-18).

Finally the item from fromInventory alot 1 is moved to toInventory on slots 0-4. The stack itemA1 has a size of 8/10, so it can take 2 of the 4 items. The second stack is of type B and can’t take this kind of item. So the remaining 2 items are added to the stack itemA2 which has now a size of 9/10 instead of 7/10. The source inventory item slot is now empty/does no longer exist.

Lets pin down the new information.

5. There can be multiple inventories.

6. Items are at slots and the position/index is relevant.

7. Items can be moved between inventories which will merge slots by their identifier.

8. Transfer logic is contained in the authority system.

Repeat :)

I will speed up a bit and write down any other points I find while going over the test methods.

removePartOfStack()

9. A stack can be removed partially.

10. The class RemoveItemAction can be removed. (the class is never used outside of tests).

11. InventorySlotStackSizeChangedEvent is fired for slot size changes.

removeWholeStack()

12. An entire stack can be removed.

13. BeforeItemRemovedFromInventory is fired before items are removed.

removePartOfStackWithDestroy()

14. Item entities can be destroyed when removed.

removeWholeStackWithDestroy()()

15. InventorySlotChangedEvent is fired when a slot changes.

removeOverOneStack()

16. Items can be removed from multiple stacks at the same time.

removeWholeStackWithVeto()

17. When BeforeItemRemovedFromInventory is consumed, no items are removed.

addItemToEmpty()

18. GiveItemAction can be removed. (the class is never used outside of tests).

19. Items can be added to an empty inventors.

20. BeforeItemPutInInventory is fired before a new item is put in the inventory.

addItemToPartial()

21. Stacks are merged when items are added.

addItemToPartialAndOverflow()

21. Overflow after the merge from stacks creates new stacks.

addItemToEmptyWithVeto()

22. When BeforeItemPutInInventory is consumed, the item is not added to the inventory.

testMoveItemToSlotsWithToLessSpaceInTargetSlots()

23. When items do not fit in a new inventory, the old items are not deleted but kept in the old inventory.

testMoveItemToSlotsWithTargetVetos()

24. BeforeItemPutInInventory is fired per slot, if one slot receives a veto, the items are moved to the next available slot.

testMoveItemToSlotsWithRemovalVeto()

25. BeforeItemRemovedFromInventory is fired per slot, if one slot receives a veto, the other items will still be removed.

testMoveItemToSlotsWithFullTargetInventorySlots()

26. If not all items fit in the target inventory, the whole transaction is not executed.

Conclusion

I think this counts as progress :) a simple testclass and we already identified 26 aspects of the inventory system. Most of them are requirements, some are just notes for later on. Nevertheless, I like to keep a list of all the points to keep in mind.

We can inspect the actual implementation of the system in the next session and maybe find some requirements of the system which are not revealed by the tests. This should give us a good preparation if we want to do a reimplementaton of the system or moving the system to a separate module or library.

Written on October 29, 2017