Skip to content

Commit

Permalink
feat(StartingInventory): Allow for nested inventories (#9)
Browse files Browse the repository at this point in the history
* feat(StartingInventory): allow for nested inventories
* doc(StartingInventory): Update JavaDoc
* feat(StartingInventory): increase default item quantity to 1
* test: Reduce starting inventory tests to (failing) assertions

Co-authored-by: jdrueckert <[email protected]>
  • Loading branch information
skaldarnar and jdrueckert authored May 23, 2020
1 parent 9251201 commit bcbdc34
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 676 deletions.
2 changes: 1 addition & 1 deletion module.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"id" : "Inventory",
"version" : "1.1.0",
"version" : "1.2.0",
"isReleaseManaged": true,
"displayName" : "Core Inventory",
"author": "The Terasology Foundation",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,6 @@ private static boolean moveToFreeSlots(EntityRef instigator, EntityRef from, int
return false;
}


static boolean moveItemToSlots(EntityRef instigator, EntityRef from, int fromSlot, EntityRef to, List<Integer> toSlots) {
EntityRef fromItem = InventoryUtils.getItemAt(from, fromSlot);
BeforeItemRemovedFromInventory removeFrom = new BeforeItemRemovedFromInventory(instigator, fromItem, fromSlot);
Expand All @@ -356,7 +355,6 @@ static boolean moveItemToSlots(EntityRef instigator, EntityRef from, int fromSlo
return movedToStack > 0 || movedToFreeSlot;
}


static boolean moveItem(EntityRef instigator, EntityRef from, int slotFrom, EntityRef to, int slotTo) {
if (checkForStacking(from, slotFrom, to, slotTo)) {
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,51 +15,50 @@
*/
package org.terasology.logic.inventory;

import com.google.common.collect.Lists;
import org.terasology.entitySystem.Component;
import org.terasology.entitySystem.prefab.Prefab;
import org.terasology.reflection.MappedContainer;
import org.terasology.world.block.Block;

import java.util.LinkedList;
import java.util.List;

/**
* A class that allows you to specify a player's starting inventory easily.
*
* <p><strong>Note</strong> it is not possible to specify nested items (e.g. items in a chest). Stackable
* <em>items</em> may have a quantity greater than their maxStackSize and they will be split into multiple
* stacks as required. However stackable <em>blocks</em> are limited to maximum stacks of 99, any excess
* is ignored.</p>
*
* <p>
* The default amount of items to add to the inventory is '1'. Not specifying a quantity explicitly will use this
* default value.
* <p>
* Stackable <em>items</em> may have a quantity greater than their maxStackSize and they will be split into multiple
* stacks as required. However stackable <em>blocks</em> are limited to maximum stacks of 99, any excess is
* ignored.</p>
* <p>
* It is also possible to specify nested items (e.g. items in a chest).
* <p>
* Example: add a delta for the player.prefab with the following
* <pre>
* {
* "StartingInventory": {
* "items": [
* { "uri": "core:pickaxe", "quantity": 1 },
* { "uri": "core:axe", "quantity": 1 },
* { "uri": "core:coal", "quantity": 149 },
* { "uri": "core:shovel", "quantity": 1 },
* { "uri": "CoreBlocks:Torch", "quantity": 99 },
* { "uri": "CoreBlocks:Torch", "quantity": 120 },
* { "uri": "core:explodeTool", "quantity": 1 },
* { "uri": "core:railgunTool", "quantity": 1 },
* { "uri": "CoreBlocks:chest", "quantity": 1 }
* { "uri": "CoreAssets:pickaxe" },
* {
* "uri": "CoreAdvancedAssets:chest",
* "items": [
* { "uri": "CoreAssets:Torch", "quantity": 99 },
* ]
* }
* ]
* }
* }
* </pre>
*/
public class StartingInventoryComponent implements Component {

public List<InventoryItem> items = new LinkedList<>();
public List<InventoryItem> items = Lists.newLinkedList();

/**
* A simple class connecting a resource (a {@link Block} or {@link Prefab}) to a quantity
*/
@MappedContainer
public static class InventoryItem {

/**
* A resource uri, may be either a block uri or an item uri.
*/
Expand All @@ -68,6 +67,14 @@ public static class InventoryItem {
/**
* Must be greater than 0.
*/
public int quantity = 0;
public int quantity = 1;

/**
* A list of objects to be nested inside this inventory item.
* <p>
* Adding inventory items to this list will cause a {@link InventoryComponent} to be added to this object. The
* nested inventory is filled with the items specified in this list.
*/
public List<InventoryItem> items = Lists.newLinkedList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,26 @@
*/
package org.terasology.logic.inventory;

import com.google.common.collect.Lists;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.entitySystem.entity.EntityManager;
import org.terasology.entitySystem.entity.EntityRef;
import org.terasology.entitySystem.event.ReceiveEvent;
import org.terasology.entitySystem.prefab.Prefab;
import org.terasology.entitySystem.prefab.PrefabManager;
import org.terasology.entitySystem.systems.BaseComponentSystem;
import org.terasology.entitySystem.systems.RegisterMode;
import org.terasology.entitySystem.systems.RegisterSystem;
import org.terasology.logic.players.event.OnPlayerSpawnedEvent;
import org.terasology.registry.In;
import org.terasology.world.block.BlockManager;
import org.terasology.world.block.family.BlockFamily;
import org.terasology.world.block.items.BlockItemFactory;

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@RegisterSystem(RegisterMode.AUTHORITY)
public class StartingInventorySystem extends BaseComponentSystem {

Expand All @@ -55,121 +59,98 @@ public void initialise() {
blockFactory = new BlockItemFactory(entityManager);
}

@ReceiveEvent(components = {InventoryComponent.class})
@ReceiveEvent
public void onStartingInventory(OnPlayerSpawnedEvent event,
EntityRef entityRef,
StartingInventoryComponent startingInventoryComponent) {
InventoryComponent inventoryComponent = entityRef.getComponent(InventoryComponent.class);
if (entityRef.getParentPrefab() != null) {
logger.info("Adding starting inventory to {}, entity has {} slots",
entityRef.getParentPrefab().getName(), inventoryComponent.itemSlots.size());
}

InventoryComponent inventoryComponent =
Optional.ofNullable(entityRef.getComponent(InventoryComponent.class))
.orElse(new InventoryComponent(40));
entityRef.addOrSaveComponent(inventoryComponent);

for (StartingInventoryComponent.InventoryItem item : startingInventoryComponent.items) {
addToInventory(entityRef, item, inventoryComponent);
if (validateItem(item)) {
addToInventory(entityRef, item, inventoryComponent);
}
}
entityRef.removeComponent(StartingInventoryComponent.class);
}

private boolean addToInventory(EntityRef entityRef,
StartingInventoryComponent.InventoryItem item,
InventoryComponent inventoryComponent) {
String uri = item.uri;
int quantity = item.quantity;
if (uri == null) {
/**
* Ensure that the item references a non-empty URI and a quantity greater than zero.
* <p>
* This method logs WARNINGs if the item could not be validated.
*
* @param item the inventory item to validate
* @return true if the item has non-empty URI and quantity greater zero, false otherwise
*/
private boolean validateItem(StartingInventoryComponent.InventoryItem item) {
if (item.uri == null || item.uri.isEmpty()) {
logger.warn("Improperly specified starting inventory item: Uri is null");
return false;
}
if (quantity <= 0) {
if (item.quantity <= 0) {
logger.warn("Improperly specified starting inventory item: quantity for '{}' less than zero ({})",
uri, quantity);
item.uri, item.quantity);
return false;
}
return addToInventory(entityRef, uri, quantity, inventoryComponent);
return true;
}

private boolean addToInventory(EntityRef entityRef,
String uri,
int quantity,
InventoryComponent inventoryComponent) {
// Determine if this is a Block or Item
logger.info("Adding {} {}", quantity, uri);
BlockFamily blockFamily = blockManager.getBlockFamily(uri);
boolean success = true;
if (blockFamily != null) {
success = tryAddBlocks(entityRef, blockFamily, quantity, inventoryComponent);
} else {
success = tryAddItems(entityRef, uri, quantity, inventoryComponent);
}
if (!success) {
logger.error("Failed to add {} {}", quantity, uri);
}
return success;
}
private void addToInventory(EntityRef entityRef,
StartingInventoryComponent.InventoryItem item,
InventoryComponent inventoryComponent) {
String uri = item.uri;
int quantity = item.quantity;

private boolean tryAddBlocks(EntityRef entityRef,
BlockFamily blockFamily,
int quantity,
InventoryComponent inventoryComponent) {
long available = availableSlots(inventoryComponent);
if (available >= 1) {
if (quantity > 99) {
logger.warn("Block stack of > 99 found. Currently maximum block stack size is 99. Adding 99");
}
return inventoryManager.giveItem(
entityRef, EntityRef.NULL, blockFactory.newInstance(blockFamily, Math.min(quantity, 99)));
}
return false;
final List<EntityRef> objects =
tryAsBlock(uri, quantity, item.items)
.map(Optional::of)
.orElseGet(() -> tryAsItem(uri, quantity))
.orElse(Lists.newArrayList());

objects.forEach(o ->
inventoryManager.giveItem(entityRef, EntityRef.NULL, o)
);
}

private boolean tryAddItems(EntityRef entityRef,
String uri,
int quantity,
InventoryComponent inventoryComponent) {
long available = availableSlots(inventoryComponent);
// Find out of the item is stackable
Prefab prefab = prefabManager.getPrefab(uri);
if (prefab == null) {
logger.error("Failed to find prefab {}", uri);
return false;
}
ItemComponent component = prefab.getComponent(ItemComponent.class);
if (component == null) {
logger.error("Failed to find ItemComponent for {}", uri);
return false;
}
if (!InventoryUtils.isStackable(component)) {
// Item is not stackable, one slot used per item
if (available >= quantity) {
for (int i = 0; i < quantity; ++i) {
inventoryManager.giveItem(entityRef, EntityRef.NULL, entityManager.create(uri));
}
} else {
logger.error("Insufficient inventory space to add {} {}", quantity, uri);
}
} else {
// Item stackable, inventory manager will handle stacking, but we still need to know
// how much we have added
int numFullStacks = quantity / component.maxStackSize;
int remainder = quantity % component.maxStackSize;

// Add full stacks
for (int i = 0; i < numFullStacks; ++i) {
for (int j = 0; j < component.maxStackSize; ++j) {
inventoryManager.giveItem(entityRef,
EntityRef.NULL, entityManager.create(uri));
}
}
/**
* Adds an {@link InventoryComponent} to the given entity holding the specified items.
*
* @param entity the entity that should hold the nested inventory
* @param items items to be filled into the nested inventory
*/
private void addNestedInventory(EntityRef entity,
List<StartingInventoryComponent.InventoryItem> items) {
InventoryComponent nestedInventory =
Optional.ofNullable(entity.getComponent(InventoryComponent.class))
.orElseGet(() -> new InventoryComponent(30));
entity.addOrSaveComponent(nestedInventory);
items.stream()
.filter(this::validateItem)
.forEach(i -> addToInventory(entity, i, nestedInventory));
}

// Add remainder
for (int i = 0; i < remainder; ++i) {
inventoryManager.giveItem(entityRef,
EntityRef.NULL, entityManager.create(uri));
}
}
return true;
private Optional<List<EntityRef>> tryAsBlock(String uri,
int quantity,
List<StartingInventoryComponent.InventoryItem> nestedItems) {
return Optional.ofNullable(blockManager.getBlockFamily(uri))
.map(blockFamily ->
Stream.generate(() -> blockFactory.newInstance(blockFamily))
.limit(quantity)
.peek(block -> {
if (!nestedItems.isEmpty()) {
addNestedInventory(block, nestedItems);
}
})
.collect(Collectors.toList()));
}

private long availableSlots(InventoryComponent inventoryComponent) {
return inventoryComponent.itemSlots.stream().filter(ref -> ref.equals(EntityRef.NULL)).count();
private Optional<List<EntityRef>> tryAsItem(String uri,
int quantity) {
return Optional.ofNullable(prefabManager.getPrefab(uri))
.filter(p -> p.hasComponent(ItemComponent.class))
.map(p -> Stream.generate(() -> entityManager.create(uri)).limit(quantity).collect(Collectors.toList()));
}
}
Loading

0 comments on commit bcbdc34

Please sign in to comment.