item stacking
All checks were successful
studiorailgun/Renderer/pipeline/head This commit looks good

This commit is contained in:
austin 2025-04-30 02:28:55 -04:00
parent dec244c509
commit 2452cf194b
15 changed files with 245 additions and 37 deletions

View File

@ -10,6 +10,7 @@
"fabPath" : "Data/fab/wood_refined_floor.block" "fabPath" : "Data/fab/wood_refined_floor.block"
}, },
"clientSideSecondary" : "PLACE_FAB", "clientSideSecondary" : "PLACE_FAB",
"maxStack" : 100,
"itemAudio": { "itemAudio": {
"uiGrabAudio" : "Audio/ui/items/specific/Pick Up Wood A.wav", "uiGrabAudio" : "Audio/ui/items/specific/Pick Up Wood A.wav",
"uiReleaseAudio" : "Audio/ui/items/specific/Drop Wood A.wav" "uiReleaseAudio" : "Audio/ui/items/specific/Drop Wood A.wav"

View File

@ -1605,6 +1605,7 @@ Cleaning up dead code
Fab items Fab items
Items keep charge state Items keep charge state
UI renders charge state UI renders charge state
Item stacking

View File

@ -315,6 +315,16 @@ public class ServerToolbarState implements BehaviorTree {
return realWorldItem; return realWorldItem;
} }
/**
* Gets the in-inventory item
* @return The item entity if it exists, null otherwise
*/
public Entity getInInventoryItem(){
RelationalInventoryState toolbarInventory = InventoryUtils.getToolbarInventory(parent);
return toolbarInventory.getItemSlot(selectedSlot + "");
}
/** /**
* Attempts to change the selection to a new value * Attempts to change the selection to a new value
* @param value The value * @param value The value

View File

@ -11,6 +11,7 @@ import electrosphere.entity.Entity;
import electrosphere.entity.btree.BehaviorTree; import electrosphere.entity.btree.BehaviorTree;
import electrosphere.entity.state.equip.ClientEquipState; import electrosphere.entity.state.equip.ClientEquipState;
import electrosphere.entity.state.equip.ClientToolbarState; import electrosphere.entity.state.equip.ClientToolbarState;
import electrosphere.entity.state.item.ClientChargeState;
import electrosphere.game.data.creature.type.equip.EquipPoint; import electrosphere.game.data.creature.type.equip.EquipPoint;
import electrosphere.net.parser.net.message.InventoryMessage; import electrosphere.net.parser.net.message.InventoryMessage;
import electrosphere.net.server.protocol.InventoryProtocol; import electrosphere.net.server.protocol.InventoryProtocol;
@ -186,6 +187,11 @@ public class ClientInventoryState implements BehaviorTree {
// throw new UnsupportedOperationException("TODO: in world item is null"); // throw new UnsupportedOperationException("TODO: in world item is null");
} }
} break; } break;
case SERVERUPDATEITEMCHARGES: {
Entity clientInventoryItem = Globals.clientSceneWrapper.getEntityFromServerId(message.getentityId());
ClientChargeState clientChargeState = ClientChargeState.getClientChargeState(clientInventoryItem);
clientChargeState.setCharges(message.getcharges());
} break;
case CLIENTREQUESTCRAFT: case CLIENTREQUESTCRAFT:
case CLIENTUPDATETOOLBAR: case CLIENTUPDATETOOLBAR:
case CLIENTREQUESTADDNATURAL: case CLIENTREQUESTADDNATURAL:

View File

@ -14,6 +14,7 @@ import electrosphere.entity.state.equip.ClientEquipState;
import electrosphere.entity.state.equip.ServerEquipState; import electrosphere.entity.state.equip.ServerEquipState;
import electrosphere.entity.state.equip.ServerToolbarState; import electrosphere.entity.state.equip.ServerToolbarState;
import electrosphere.entity.state.gravity.GravityUtils; import electrosphere.entity.state.gravity.GravityUtils;
import electrosphere.entity.state.item.ServerChargeState;
import electrosphere.entity.types.creature.CreatureUtils; import electrosphere.entity.types.creature.CreatureUtils;
import electrosphere.entity.types.item.ItemUtils; import electrosphere.entity.types.item.ItemUtils;
import electrosphere.game.data.item.Item; import electrosphere.game.data.item.Item;
@ -173,16 +174,86 @@ public class InventoryUtils {
if(item == null){ if(item == null){
throw new Error("Null item provided! " + item); throw new Error("Null item provided! " + item);
} }
boolean creatureIsCreature = CreatureUtils.isCreature(creature); if(!CreatureUtils.isCreature(creature)){
boolean itemIsItem = ItemUtils.isItem(item); throw new Error("Creature is not a creature!");
boolean hasInventory = hasNaturalInventory(creature); }
//check if the item is already in an inventory if(!ItemUtils.isItem(item)){
boolean itemIsInInventory = ItemUtils.itemIsInInventory(item); throw new Error("Item is not an item!");
if(creatureIsCreature && itemIsItem && hasInventory && !itemIsInInventory){ }
if(!InventoryUtils.hasNaturalInventory(creature)){
throw new Error("Creature does not have a natural inventory");
}
if(ItemUtils.itemIsInInventory(item)){
throw new Error("Item is already in an inventory!");
}
Item itemData = Globals.gameConfigCurrent.getItemMap().getItem(item);
//get inventory //get inventory
//for the moment we're just gonna get natural inventory //for the moment we're just gonna get natural inventory
//later we'll need to search through all creature inventories to find the item //later we'll need to search through all creature inventories to find the item
UnrelationalInventoryState inventory = getNaturalInventory(creature); UnrelationalInventoryState inventory = InventoryUtils.getNaturalInventory(creature);
//check if it should be added to an existing stack
Entity foundExisting = null;
if(itemData.getMaxStack() != null){
RelationalInventoryState toolbarInventory = InventoryUtils.getToolbarInventory(creature);
if(toolbarInventory != null){
for(Entity toolbarItem : toolbarInventory.getItems()){
if(toolbarItem == null){
continue;
}
Item toolbarData = Globals.gameConfigCurrent.getItemMap().getItem(toolbarItem);
if(!toolbarData.getId().equals(itemData.getId())){
continue;
}
ServerChargeState serverChargeState = ServerChargeState.getServerChargeState(toolbarItem);
if(serverChargeState.getCharges() >= itemData.getMaxStack()){
continue;
}
foundExisting = toolbarItem;
break;
}
}
if(foundExisting == null){
for(Entity naturalItem : inventory.getItems()){
if(naturalItem == null){
continue;
}
Item toolbarData = Globals.gameConfigCurrent.getItemMap().getItem(naturalItem);
if(!toolbarData.getId().equals(itemData.getId())){
continue;
}
ServerChargeState serverChargeState = ServerChargeState.getServerChargeState(naturalItem);
if(serverChargeState.getCharges() >= itemData.getMaxStack()){
continue;
}
foundExisting = naturalItem;
break;
}
}
}
//increase charges
if(foundExisting != null){
ServerChargeState serverChargeState = ServerChargeState.getServerChargeState(foundExisting);
serverChargeState.setCharges(serverChargeState.getCharges() + 1);
//if we are the server, immediately send required packets
ServerDataCell dataCell = DataCellSearchUtils.getEntityDataCell(item);
dataCell.broadcastNetworkMessage(EntityMessage.constructDestroyMessage(item.getId()));
//tell player that their item has another charge
if(CreatureUtils.hasControllerPlayerId(creature)){
//get the player
int controllerPlayerID = CreatureUtils.getControllerPlayerId(creature);
Player controllerPlayer = Globals.playerManager.getPlayerFromId(controllerPlayerID);
//send message
controllerPlayer.addMessage(InventoryMessage.constructserverUpdateItemChargesMessage(foundExisting.getId(), serverChargeState.getCharges()));
}
//alert script engine
ServerScriptUtils.fireSignalOnEntity(creature, "itemPickup", item.getId(), foundExisting.getId());
//destroy the item that was left over
ServerEntityUtils.destroyEntity(item);
return foundExisting;
}
//destroy in-world entity and create in-inventory item //destroy in-world entity and create in-inventory item
//we're doing this so that we're not constantly sending networking messages for invisible entities attached to the player //we're doing this so that we're not constantly sending networking messages for invisible entities attached to the player
Entity inventoryItem = ItemUtils.serverRecreateContainerItem(item, creature); Entity inventoryItem = ItemUtils.serverRecreateContainerItem(item, creature);
@ -193,7 +264,7 @@ public class InventoryUtils {
//if we are the server, immediately send required packets //if we are the server, immediately send required packets
ServerDataCell dataCell = DataCellSearchUtils.getEntityDataCell(item); ServerDataCell dataCell = DataCellSearchUtils.getEntityDataCell(item);
// ServerDataCell dataCell = Globals.dataCellLocationResolver.getDataCellAtPoint(EntityUtils.getPosition(item),item); // ServerDataCell dataCell = Globals.dataCellLocationResolver.getDataCellAtPoint(EntityUtils.getPosition(item),item);
//broadcast destroy entity //broadcast destroy entityq
dataCell.broadcastNetworkMessage(EntityMessage.constructDestroyMessage(item.getId())); dataCell.broadcastNetworkMessage(EntityMessage.constructDestroyMessage(item.getId()));
//tell controlling player that they have an item in their inventory //tell controlling player that they have an item in their inventory
if(CreatureUtils.hasControllerPlayerId(creature)){ if(CreatureUtils.hasControllerPlayerId(creature)){
@ -209,8 +280,6 @@ public class InventoryUtils {
ServerEntityUtils.destroyEntity(item); ServerEntityUtils.destroyEntity(item);
return inventoryItem; return inventoryItem;
} }
return null;
}
/** /**
* Perform the entity transforms to actually store an item in an inventory, if server this has the side effect of also sending packets on success * Perform the entity transforms to actually store an item in an inventory, if server this has the side effect of also sending packets on success

View File

@ -94,6 +94,7 @@ public class ServerInventoryState implements BehaviorTree {
case SERVERCOMMANDUNEQUIPITEM: case SERVERCOMMANDUNEQUIPITEM:
case SERVERCOMMANDEQUIPITEM: case SERVERCOMMANDEQUIPITEM:
case SERVERCOMMANDMOVEITEMCONTAINER: case SERVERCOMMANDMOVEITEMCONTAINER:
case SERVERUPDATEITEMCHARGES:
break; break;
} }
} }

View File

@ -2,6 +2,7 @@ package electrosphere.entity.state.item;
import electrosphere.entity.btree.BehaviorTree; import electrosphere.entity.btree.BehaviorTree;
import electrosphere.entity.state.equip.ServerToolbarState;
import electrosphere.engine.Globals; import electrosphere.engine.Globals;
import electrosphere.entity.Entity; import electrosphere.entity.Entity;
import electrosphere.entity.EntityDataStrings; import electrosphere.entity.EntityDataStrings;
@ -47,6 +48,29 @@ public class ServerChargeState implements BehaviorTree {
this.charges = 1; this.charges = 1;
} }
@Override
public void simulate(float deltaTime) {
}
/**
* Attempts to remove a charge from whatever item the parent entity currently has equipped
* @param parent The parent
*/
public static void attemptRemoveCharges(Entity parent, int charges){
if(ServerToolbarState.hasServerToolbarState(parent)){
ServerToolbarState serverToolbarState = ServerToolbarState.getServerToolbarState(parent);
Entity inventoryItem = serverToolbarState.getInInventoryItem();
if(inventoryItem != null){
ServerChargeState serverChargeState = ServerChargeState.getServerChargeState(inventoryItem);
if(serverChargeState.getCharges() - charges > 0){
serverChargeState.setCharges(serverChargeState.getCharges() - charges);
} else {
throw new Error("Undefined! " + charges + serverChargeState.getCharges());
}
}
}
}
/** /**
* <p> (initially) Automatically generated </p> * <p> (initially) Automatically generated </p>
* <p> * <p>
@ -124,8 +148,4 @@ public class ServerChargeState implements BehaviorTree {
return charges; return charges;
} }
@Override
public void simulate(float deltaTime) {
}
} }

View File

@ -41,6 +41,8 @@ import electrosphere.net.parser.net.message.NetworkMessage;
import electrosphere.net.server.player.Player; import electrosphere.net.server.player.Player;
import electrosphere.renderer.actor.Actor; import electrosphere.renderer.actor.Actor;
import electrosphere.server.datacell.Realm; import electrosphere.server.datacell.Realm;
import electrosphere.server.datacell.utils.EntityLookupUtils;
import electrosphere.server.datacell.utils.ServerBehaviorTreeUtils;
import electrosphere.server.datacell.utils.ServerEntityTagUtils; import electrosphere.server.datacell.utils.ServerEntityTagUtils;
import electrosphere.server.entity.poseactor.PoseActor; import electrosphere.server.entity.poseactor.PoseActor;
@ -503,19 +505,27 @@ public class ItemUtils {
rVal.putData(EntityDataStrings.ITEM_EQUIP_WHITELIST, getEquipWhitelist(item)); rVal.putData(EntityDataStrings.ITEM_EQUIP_WHITELIST, getEquipWhitelist(item));
} }
//
//stacking behavior
//
if(itemData.getMaxStack() != null){
ClientChargeState.attachTree(rVal, itemData.getMaxStack());
}
rVal.putData(EntityDataStrings.ITEM_ICON,ItemUtils.getItemIcon(item)); rVal.putData(EntityDataStrings.ITEM_ICON,ItemUtils.getItemIcon(item));
rVal.putData(EntityDataStrings.ITEM_EQUIP_CLASS, item.getData(EntityDataStrings.ITEM_EQUIP_CLASS)); rVal.putData(EntityDataStrings.ITEM_EQUIP_CLASS, item.getData(EntityDataStrings.ITEM_EQUIP_CLASS));
CommonEntityUtils.setEntityType(rVal, EntityType.ITEM); CommonEntityUtils.setEntityType(rVal, EntityType.ITEM);
rVal.putData(EntityDataStrings.ITEM_IS_IN_INVENTORY, true); rVal.putData(EntityDataStrings.ITEM_IS_IN_INVENTORY, true);
ItemUtils.setContainingParent(rVal, containingParent); ItemUtils.setContainingParent(rVal, containingParent);
CommonEntityUtils.setEntitySubtype(rVal, CommonEntityUtils.getEntitySubtype(item)); CommonEntityUtils.setEntitySubtype(rVal, CommonEntityUtils.getEntitySubtype(item));
//attach to server tracking
Realm realm = Globals.realmManager.getEntityRealm(item);
EntityLookupUtils.registerServerEntity(rVal);
ServerBehaviorTreeUtils.registerEntity(rVal);
Globals.realmManager.mapEntityToRealm(rVal, realm);
Globals.entityDataCellMapper.registerEntity(rVal, realm.getInventoryCell());
//
//stacking behavior
//
if(itemData.getMaxStack() != null){
ServerChargeState.attachTree(rVal, itemData.getMaxStack());
}
return rVal; return rVal;
} else { } else {
return null; return null;

View File

@ -64,8 +64,16 @@ public class InventoryProtocol implements ClientProtocolTemplate<InventoryMessag
inventoryState.addNetworkMessage(message); inventoryState.addNetworkMessage(message);
} }
} }
} break;
case SERVERUPDATEITEMCHARGES: {
LoggerInterface.loggerNetworking.DEBUG("[CLIENT] SET CHARGES OF " + message.getentityId());
if(Globals.playerEntity != null){
ClientInventoryState inventoryState;
if((inventoryState = InventoryUtils.clientGetInventoryState(Globals.playerEntity))!=null){
inventoryState.addNetworkMessage(message);
} }
break; }
} break;
case CLIENTREQUESTCRAFT: case CLIENTREQUESTCRAFT:
case CLIENTUPDATETOOLBAR: case CLIENTUPDATETOOLBAR:
case CLIENTREQUESTADDNATURAL: case CLIENTREQUESTADDNATURAL:

View File

@ -23,6 +23,7 @@ public class InventoryMessage extends NetworkMessage {
CLIENTUPDATETOOLBAR, CLIENTUPDATETOOLBAR,
CLIENTREQUESTPERFORMITEMACTION, CLIENTREQUESTPERFORMITEMACTION,
CLIENTREQUESTCRAFT, CLIENTREQUESTCRAFT,
SERVERUPDATEITEMCHARGES,
} }
/** /**
@ -42,6 +43,7 @@ public class InventoryMessage extends NetworkMessage {
double viewTargetZ; double viewTargetZ;
int stationId; int stationId;
int recipeId; int recipeId;
int charges;
/** /**
* Constructor * Constructor
@ -245,6 +247,20 @@ public class InventoryMessage extends NetworkMessage {
this.recipeId = recipeId; this.recipeId = recipeId;
} }
/**
* Gets charges
*/
public int getcharges() {
return charges;
}
/**
* Sets charges
*/
public void setcharges(int charges) {
this.charges = charges;
}
/** /**
* Removes the packet header from the buffer * Removes the packet header from the buffer
* @param byteBuffer The buffer * @param byteBuffer The buffer
@ -305,6 +321,12 @@ public class InventoryMessage extends NetworkMessage {
} else { } else {
return false; return false;
} }
case TypeBytes.INVENTORY_MESSAGE_TYPE_SERVERUPDATEITEMCHARGES:
if(byteBuffer.getRemaining() >= TypeBytes.INVENTORY_MESSAGE_TYPE_SERVERUPDATEITEMCHARGES_SIZE){
return true;
} else {
return false;
}
} }
return false; return false;
} }
@ -806,6 +828,29 @@ public class InventoryMessage extends NetworkMessage {
return rVal; return rVal;
} }
/**
* Parses a message of type serverUpdateItemCharges
*/
public static InventoryMessage parseserverUpdateItemChargesMessage(CircularByteBuffer byteBuffer, MessagePool pool){
InventoryMessage rVal = (InventoryMessage)pool.get(MessageType.INVENTORY_MESSAGE);
rVal.messageType = InventoryMessageType.SERVERUPDATEITEMCHARGES;
InventoryMessage.stripPacketHeader(byteBuffer);
rVal.setentityId(ByteStreamUtils.popIntFromByteQueue(byteBuffer));
rVal.setcharges(ByteStreamUtils.popIntFromByteQueue(byteBuffer));
return rVal;
}
/**
* Constructs a message of type serverUpdateItemCharges
*/
public static InventoryMessage constructserverUpdateItemChargesMessage(int entityId,int charges){
InventoryMessage rVal = new InventoryMessage(InventoryMessageType.SERVERUPDATEITEMCHARGES);
rVal.setentityId(entityId);
rVal.setcharges(charges);
rVal.serialize();
return rVal;
}
@Override @Override
void serialize(){ void serialize(){
byte[] intValues = new byte[8]; byte[] intValues = new byte[8];
@ -1047,6 +1092,21 @@ public class InventoryMessage extends NetworkMessage {
rawBytes[10+i] = intValues[i]; rawBytes[10+i] = intValues[i];
} }
break; break;
case SERVERUPDATEITEMCHARGES:
rawBytes = new byte[2+4+4];
//message header
rawBytes[0] = TypeBytes.MESSAGE_TYPE_INVENTORY;
//entity messaage header
rawBytes[1] = TypeBytes.INVENTORY_MESSAGE_TYPE_SERVERUPDATEITEMCHARGES;
intValues = ByteStreamUtils.serializeIntToBytes(entityId);
for(int i = 0; i < 4; i++){
rawBytes[2+i] = intValues[i];
}
intValues = ByteStreamUtils.serializeIntToBytes(charges);
for(int i = 0; i < 4; i++){
rawBytes[6+i] = intValues[i];
}
break;
} }
serialized = true; serialized = true;
} }

View File

@ -410,6 +410,11 @@ public abstract class NetworkMessage {
rVal = InventoryMessage.parseclientRequestCraftMessage(byteBuffer,pool); rVal = InventoryMessage.parseclientRequestCraftMessage(byteBuffer,pool);
} }
break; break;
case TypeBytes.INVENTORY_MESSAGE_TYPE_SERVERUPDATEITEMCHARGES:
if(InventoryMessage.canParseMessage(byteBuffer,secondByte)){
rVal = InventoryMessage.parseserverUpdateItemChargesMessage(byteBuffer,pool);
}
break;
} }
break; break;
case TypeBytes.MESSAGE_TYPE_SYNCHRONIZATION: case TypeBytes.MESSAGE_TYPE_SYNCHRONIZATION:

View File

@ -164,6 +164,7 @@ public class TypeBytes {
public static final byte INVENTORY_MESSAGE_TYPE_CLIENTUPDATETOOLBAR = 9; public static final byte INVENTORY_MESSAGE_TYPE_CLIENTUPDATETOOLBAR = 9;
public static final byte INVENTORY_MESSAGE_TYPE_CLIENTREQUESTPERFORMITEMACTION = 10; public static final byte INVENTORY_MESSAGE_TYPE_CLIENTREQUESTPERFORMITEMACTION = 10;
public static final byte INVENTORY_MESSAGE_TYPE_CLIENTREQUESTCRAFT = 11; public static final byte INVENTORY_MESSAGE_TYPE_CLIENTREQUESTCRAFT = 11;
public static final byte INVENTORY_MESSAGE_TYPE_SERVERUPDATEITEMCHARGES = 12;
/* /*
Inventory packet sizes Inventory packet sizes
*/ */
@ -172,6 +173,7 @@ public class TypeBytes {
public static final byte INVENTORY_MESSAGE_TYPE_CLIENTREQUESTADDNATURAL_SIZE = 6; public static final byte INVENTORY_MESSAGE_TYPE_CLIENTREQUESTADDNATURAL_SIZE = 6;
public static final byte INVENTORY_MESSAGE_TYPE_CLIENTUPDATETOOLBAR_SIZE = 6; public static final byte INVENTORY_MESSAGE_TYPE_CLIENTUPDATETOOLBAR_SIZE = 6;
public static final byte INVENTORY_MESSAGE_TYPE_CLIENTREQUESTCRAFT_SIZE = 14; public static final byte INVENTORY_MESSAGE_TYPE_CLIENTREQUESTCRAFT_SIZE = 14;
public static final byte INVENTORY_MESSAGE_TYPE_SERVERUPDATEITEMCHARGES_SIZE = 10;
/* /*
Synchronization subcategories Synchronization subcategories

View File

@ -106,6 +106,7 @@ public class InventoryProtocol implements ServerProtocolTemplate<InventoryMessag
case SERVERCOMMANDUNEQUIPITEM: case SERVERCOMMANDUNEQUIPITEM:
case SERVERCOMMANDMOVEITEMCONTAINER: case SERVERCOMMANDMOVEITEMCONTAINER:
case SERVERCOMMANDEQUIPITEM: case SERVERCOMMANDEQUIPITEM:
case SERVERUPDATEITEMCHARGES:
//silently ignore //silently ignore
break; break;
} }

View File

@ -13,6 +13,7 @@ import electrosphere.client.block.BlockChunkData;
import electrosphere.client.terrain.cache.ChunkData; import electrosphere.client.terrain.cache.ChunkData;
import electrosphere.engine.Globals; import electrosphere.engine.Globals;
import electrosphere.entity.Entity; import electrosphere.entity.Entity;
import electrosphere.entity.state.item.ServerChargeState;
import electrosphere.logger.LoggerInterface; import electrosphere.logger.LoggerInterface;
import electrosphere.net.parser.net.message.TerrainMessage; import electrosphere.net.parser.net.message.TerrainMessage;
import electrosphere.net.server.ServerConnectionHandler; import electrosphere.net.server.ServerConnectionHandler;
@ -98,6 +99,7 @@ public class TerrainProtocol implements ServerProtocolTemplate<TerrainMessage> {
Entity targetEntity = EntityLookupUtils.getEntityById(connectionHandler.getPlayerEntityId()); Entity targetEntity = EntityLookupUtils.getEntityById(connectionHandler.getPlayerEntityId());
Realm playerRealm = Globals.realmManager.getEntityRealm(targetEntity); Realm playerRealm = Globals.realmManager.getEntityRealm(targetEntity);
ServerBlockEditing.placeBlockFab(playerRealm, worldPos, blockPos, message.getblockRotation(), message.getfabPath()); ServerBlockEditing.placeBlockFab(playerRealm, worldPos, blockPos, message.getblockRotation(), message.getfabPath());
ServerChargeState.attemptRemoveCharges(targetEntity, 1);
} break; } break;
//all ignored message types //all ignored message types
case UPDATEFLUIDDATA: case UPDATEFLUIDDATA:

View File

@ -56,6 +56,10 @@
{ {
"name" : "recipeId", "name" : "recipeId",
"type" : "FIXED_INT" "type" : "FIXED_INT"
},
{
"name" : "charges",
"type" : "FIXED_INT"
} }
], ],
"messageTypes" : [ "messageTypes" : [
@ -159,6 +163,14 @@
"stationId", "stationId",
"recipeId" "recipeId"
] ]
},
{
"messageName" : "serverUpdateItemCharges",
"description" : "Server tells client that its item has a set number of charges",
"data" : [
"entityId",
"charges"
]
} }
] ]
} }