Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import net.minecraft.inventory.Slot;
import net.minecraft.item.ItemStack;

import de.eydamos.backpack.Backpack;
import de.eydamos.backpack.inventory.ISaveableInventory;
import de.eydamos.backpack.inventory.slot.SlotCraftingAdvanced;
import de.eydamos.backpack.inventory.slot.SlotPhantom;
Expand Down Expand Up @@ -79,6 +80,11 @@ public void onContainerClosed(EntityPlayer entityPlayer) {
if (inventory != null) {
inventory.closeInventory();
}

if (backpackSave != null) {
Backpack.proxy.invalidateBackpackCache(backpackSave.getUUID());
}

super.onContainerClosed(entityPlayer);
}

Expand Down
21 changes: 15 additions & 6 deletions src/main/java/de/eydamos/backpack/item/ItemBackpackBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import net.minecraft.inventory.IInventory;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.util.EnumChatFormatting;
import net.minecraft.util.StatCollector;
import net.minecraft.world.World;
Expand All @@ -19,6 +18,7 @@
import cpw.mods.fml.relauncher.SideOnly;
import de.eydamos.backpack.helper.GuiHelper;
import de.eydamos.backpack.inventory.InventoryBackpack;
import de.eydamos.backpack.misc.BackpackUsageCache;
import de.eydamos.backpack.misc.ConfigurationBackpack;
import de.eydamos.backpack.misc.Constants;
import de.eydamos.backpack.misc.Localizations;
Expand Down Expand Up @@ -184,11 +184,20 @@ public void addInformation(ItemStack itemStack, EntityPlayer entityPlayer, List
EnumChatFormatting.YELLOW + StatCollector.translateToLocal(Localizations.TIER)
+ " "
+ (itemStack.getItemDamage() / 100 + 1));
BackpackSave backpackSave = new BackpackSave(itemStack);
NBTTagList itemList = backpackSave.getInventory(Constants.NBT.INVENTORY_BACKPACK);
int used = itemList.tagCount();
int size = backpackSave.getSize();
information.add(used + "/" + size + ' ' + StatCollector.translateToLocal(Localizations.SLOTS_USED));

if (itemStack.stackTagCompound == null) return;

String uuid = itemStack.stackTagCompound.getString(Constants.NBT.UID);
if (uuid == null) return;

BackpackUsageCache.BackpackSlotUsageInfo info = BackpackUsageCache.getBackpackInfo(uuid);

if (info != null) {
String slotsText = StatCollector.translateToLocal(Localizations.SLOTS_USED);
information.add(info.usedSlots + "/" + info.totalSlots + ' ' + slotsText);
} else {
BackpackUsageCache.requestBackpackInfo(uuid);
}
}
} else {
information.add(StatCollector.translateToLocal(Localizations.MORE_INFORMATION));
Expand Down
190 changes: 190 additions & 0 deletions src/main/java/de/eydamos/backpack/misc/BackpackUsageCache.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package de.eydamos.backpack.misc;

import java.util.Comparator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import de.eydamos.backpack.Backpack;
import de.eydamos.backpack.network.message.MessageBackpackInfoRequest;

@SideOnly(Side.CLIENT)
public class BackpackUsageCache {

private static final Map<String, CacheEntry> cache = new ConcurrentHashMap<>();
private static final Map<String, Long> requestTimestamps = new ConcurrentHashMap<>();

// Configuration constants
private static final long CACHE_DURATION_MS = 60000; // 1 minute
private static final long REQUEST_THROTTLE_MS = 1000; // 1 second throttle between requests
private static final int MAX_CACHE_SIZE = 100; // Prevent memory leaks

// Lock is only needed for the compound "put-and-evict" operation
private static final ReadWriteLock cacheLock = new ReentrantReadWriteLock();

private static class CacheEntry {

public final BackpackSlotUsageInfo info;
public final long timestamp;

public CacheEntry(BackpackSlotUsageInfo info, long timestamp) {
this.info = info;
this.timestamp = timestamp;
}

public boolean isExpired() {
return (System.currentTimeMillis() - timestamp) > CACHE_DURATION_MS;
}
}

public static class BackpackSlotUsageInfo {

public final int usedSlots;
public final int totalSlots;

public BackpackSlotUsageInfo(int used, int total) {
if (used < 0 || total < 0 || used > total) {
throw new IllegalArgumentException("Invalid backpack slot values: used=" + used + ", total=" + total);
}
this.usedSlots = used;
this.totalSlots = total;
}

@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
BackpackSlotUsageInfo that = (BackpackSlotUsageInfo) obj;
return usedSlots == that.usedSlots && totalSlots == that.totalSlots;
}

@Override
public int hashCode() {
return 31 * usedSlots + totalSlots;
}

@Override
public String toString() {
return "BackpackInfo{used=" + usedSlots + ", total=" + totalSlots + "}";
}
}

/**
* Updates backpack information in the cache
*
* @param uuid The backpack UUID
* @param used Number of used slots
* @param total Total number of slots
*/
public static void updateBackpackInfo(String uuid, int used, int total) {
if (uuid == null || uuid.trim().isEmpty()) {
throw new IllegalArgumentException("UUID cannot be null or empty");
}

BackpackSlotUsageInfo info = new BackpackSlotUsageInfo(used, total);
long currentTime = System.currentTimeMillis();
CacheEntry entry = new CacheEntry(info, currentTime);

// A write lock is required here to make the "put-and-evict" operation atomic.
cacheLock.writeLock().lock();
try {
cache.put(uuid, entry);

if (cache.size() > MAX_CACHE_SIZE) {
evictOldestEntries();
}
} finally {
cacheLock.writeLock().unlock();
}
}

/**
* Retrieves backpack information from cache. If an entry is found to be expired, it is removed
*
* @param uuid The backpack UUID
* @return BackpackSlotUsageInfo if valid and not expired, null otherwise
*/
public static BackpackSlotUsageInfo getBackpackInfo(String uuid) {
if (uuid == null || uuid.trim().isEmpty()) {
return null;
}

CacheEntry entry = cache.get(uuid);

if (entry == null) {
return null;
}

if (entry.isExpired()) {
invalidate(uuid);
return null;
}

return entry.info;
}

/**
* Requests backpack information from the server if not present in the cache, respecting a throttle to prevent
* spamming requests.
*
* @param uuid The player UUID
*/
public static void requestBackpackInfo(String uuid) {
if (uuid == null || uuid.trim().isEmpty()) {
return;
}

if (getBackpackInfo(uuid) != null) {
return;
}

long currentTime = System.currentTimeMillis();

requestTimestamps.compute(uuid, (key, lastRequestTime) -> {
// Throttle check
if (lastRequestTime != null && (currentTime - lastRequestTime) < REQUEST_THROTTLE_MS) {
return lastRequestTime;
}

Backpack.packetHandler.networkWrapper.sendToServer(new MessageBackpackInfoRequest(key));
return currentTime;
});
}

/**
* Invalidates a specific UUID from the cache
*
* @param uuid The player UUID to invalidate
*/
public static void invalidate(String uuid) {
if (uuid == null || uuid.trim().isEmpty()) {
return;
}

cache.remove(uuid);
requestTimestamps.remove(uuid);
}

/**
* Evicts the oldest entries when cache size exceeds the limit. WARNING: This method must be called from within a
* write-locked context.
*/
private static void evictOldestEntries() {
// This check is a safeguard, but the caller should ensure the lock is held.
if (cache.size() <= MAX_CACHE_SIZE) {
return;
}

// Find the oldest entries to evict (evict 10% of cache)
int entriesToEvict = Math.max(1, cache.size() / 10);

cache.entrySet().stream().sorted(Map.Entry.comparingByValue(Comparator.comparingLong(e -> e.timestamp)))
.limit(entriesToEvict).map(Map.Entry::getKey).forEach(key -> {
cache.remove(key);
requestTimestamps.remove(key);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import cpw.mods.fml.common.network.simpleimpl.SimpleNetworkWrapper;
import cpw.mods.fml.relauncher.Side;
import de.eydamos.backpack.misc.Constants;
import de.eydamos.backpack.network.message.MessageBackpackInfo;
import de.eydamos.backpack.network.message.MessageBackpackInfoRequest;
import de.eydamos.backpack.network.message.MessageGuiCommand;
import de.eydamos.backpack.network.message.MessageOpenBackpack;
import de.eydamos.backpack.network.message.MessageOpenGui;
Expand All @@ -23,10 +25,13 @@ public void initialise() {
networkWrapper.registerMessage(MessageGuiCommand.class, MessageGuiCommand.class, 2, Side.SERVER);
networkWrapper.registerMessage(MessagePersonalBackpack.class, MessagePersonalBackpack.class, 3, Side.SERVER);
networkWrapper.registerMessage(MessageRecipe.class, MessageRecipe.class, 4, Side.SERVER);
networkWrapper
.registerMessage(MessageBackpackInfoRequest.class, MessageBackpackInfoRequest.class, 5, Side.SERVER);

// these packages are send from the server to the client
networkWrapper.registerMessage(MessageOpenPersonalSlot.class, MessageOpenPersonalSlot.class, 10, Side.CLIENT);
networkWrapper.registerMessage(MessageOpenBackpack.class, MessageOpenBackpack.class, 11, Side.CLIENT);
networkWrapper.registerMessage(MessagePersonalBackpack.class, MessagePersonalBackpack.class, 12, Side.CLIENT);
networkWrapper.registerMessage(MessageBackpackInfo.class, MessageBackpackInfo.class, 13, Side.CLIENT);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package de.eydamos.backpack.network.message;

import cpw.mods.fml.common.network.simpleimpl.IMessage;
import cpw.mods.fml.common.network.simpleimpl.IMessageHandler;
import cpw.mods.fml.common.network.simpleimpl.MessageContext;
import de.eydamos.backpack.misc.BackpackUsageCache;
import io.netty.buffer.ByteBuf;

/**
* Used to pass to the client the slot usage information for a specific backpack.
*/
public class MessageBackpackInfo implements IMessage, IMessageHandler<MessageBackpackInfo, IMessage> {

private String backpackUUID;
private int usedSlots;
private int totalSlots;

public MessageBackpackInfo() {}

public MessageBackpackInfo(String uuid, int used, int total) {
this.backpackUUID = uuid;
this.usedSlots = used;
this.totalSlots = total;
}

@Override
public void fromBytes(ByteBuf buf) {
int length = buf.readInt();
this.backpackUUID = new String(buf.readBytes(length).array());
this.usedSlots = buf.readInt();
this.totalSlots = buf.readInt();
}

@Override
public void toBytes(ByteBuf buf) {
buf.writeInt(backpackUUID.length());
buf.writeBytes(backpackUUID.getBytes());
buf.writeInt(usedSlots);
buf.writeInt(totalSlots);
}

@Override
public IMessage onMessage(MessageBackpackInfo message, MessageContext ctx) {
BackpackUsageCache.updateBackpackInfo(message.backpackUUID, message.usedSlots, message.totalSlots);
return null;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package de.eydamos.backpack.network.message;

import net.minecraft.nbt.NBTTagCompound;

import cpw.mods.fml.common.network.simpleimpl.IMessage;
import cpw.mods.fml.common.network.simpleimpl.IMessageHandler;
import cpw.mods.fml.common.network.simpleimpl.MessageContext;
import de.eydamos.backpack.Backpack;
import de.eydamos.backpack.misc.Constants;
import de.eydamos.backpack.saves.BackpackSave;
import io.netty.buffer.ByteBuf;

/**
* Request from the client to ask the server to send him the slot usage information about a backpack.
*/
public class MessageBackpackInfoRequest implements IMessage, IMessageHandler<MessageBackpackInfoRequest, IMessage> {

private String backpackUUID;

public MessageBackpackInfoRequest() {}

public MessageBackpackInfoRequest(String uuid) {
this.backpackUUID = uuid;
}

@Override
public void fromBytes(ByteBuf buf) {
int length = buf.readInt();
this.backpackUUID = new String(buf.readBytes(length).array());
}

@Override
public void toBytes(ByteBuf buf) {
buf.writeInt(backpackUUID.length());
buf.writeBytes(backpackUUID.getBytes());
}

@Override
public IMessage onMessage(MessageBackpackInfoRequest message, MessageContext ctx) {
NBTTagCompound backpack = Backpack.saveFileHandler.loadBackpack(message.backpackUUID);

BackpackSave backpackSave = new BackpackSave(backpack);

int used = backpackSave.getInventory(Constants.NBT.INVENTORY_BACKPACK).tagCount();
int total = backpackSave.getSize();

return new MessageBackpackInfo(message.backpackUUID, used, total);
}
}
6 changes: 6 additions & 0 deletions src/main/java/de/eydamos/backpack/proxy/ClientProxy.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import de.eydamos.backpack.gui.GuiWorkbenchBackpack;
import de.eydamos.backpack.handler.EventHandlerClientOnly;
import de.eydamos.backpack.handler.KeyInputHandler;
import de.eydamos.backpack.misc.BackpackUsageCache;
import de.eydamos.backpack.misc.ConfigurationBackpack;
import de.eydamos.backpack.misc.Constants;
import de.eydamos.backpack.nei.OverlayHandlerBackpack;
Expand Down Expand Up @@ -50,4 +51,9 @@ public void addNeiSupport() {
FMLLog.log(Constants.MOD_ID, Level.INFO, "[Backpacks] NEI Support couldn't be enabled");
}
}

@Override
public void invalidateBackpackCache(String uuid) {
BackpackUsageCache.invalidate(uuid);
}
}
Loading