Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import fr.maxlego08.zauctionhouse.api.services.AuctionRemoveService;
import fr.maxlego08.zauctionhouse.api.services.AuctionSellService;
import fr.maxlego08.zauctionhouse.api.cache.PlayerCache;
import it.unimi.dsi.fastutil.ints.IntList;
import org.bukkit.entity.Player;

import java.util.Comparator;
Expand Down Expand Up @@ -76,6 +77,15 @@ public interface AuctionManager {
*/
List<Item> getItems(StorageType storageType);

/**
* Resolves cached item identifiers back to their live instances from the storage map.
*
* @param storageType logical container to read from
* @param ids cached identifiers to resolve
* @return resolved items in the same order as provided ids when possible
*/
List<Item> resolveItems(StorageType storageType, IntList ids);

/**
* Retrieves items from the given storage type and filters them before returning. Filtering is
* executed synchronously on the current thread and should remain inexpensive.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@
import fr.maxlego08.zauctionhouse.api.economy.AuctionEconomy;
import fr.maxlego08.zauctionhouse.api.item.Item;
import fr.maxlego08.zauctionhouse.api.item.SortItem;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;

import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;

public enum PlayerCacheKey {

ITEMS_LISTED(new TypeToken<List<Item>>() {}, Collections::emptyList),
ITEMS_EXPIRED(new TypeToken<List<Item>>() {}, Collections::emptyList),
ITEMS_PURCHASED(new TypeToken<List<Item>>() {}, Collections::emptyList),
ITEMS_OWNED(new TypeToken<List<Item>>() {}, Collections::emptyList),
ITEMS_LISTED(new TypeToken<IntList>() {}, IntArrayList::new),
ITEMS_EXPIRED(new TypeToken<IntList>() {}, IntArrayList::new),
ITEMS_PURCHASED(new TypeToken<IntList>() {}, IntArrayList::new),
ITEMS_OWNED(new TypeToken<IntList>() {}, IntArrayList::new),
ADMIN_TARGET(new TypeToken<java.util.UUID>() {}, () -> null),
ADMIN_TARGET_NAME(new TypeToken<String>() {}, () -> ""),
ITEM_SHOW(new TypeToken<Item>() {}, () -> null),
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ allprojects {
implementation("fr.maxlego08.sarah:sarah:1.20")
implementation("com.tcoded:FoliaLib:0.5.1")
implementation("fr.traqueur.currencies:currenciesapi:1.0.11")
implementation("it.unimi.dsi:fastutil:8.5.14")
}
}

Expand Down
180 changes: 140 additions & 40 deletions src/main/java/fr/maxlego08/zauctionhouse/ZAuctionManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,18 @@
import fr.maxlego08.zauctionhouse.services.SellService;
import fr.maxlego08.zauctionhouse.utils.ZUtils;
import fr.maxlego08.zauctionhouse.utils.cache.ZPlayerCache;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -49,7 +55,10 @@ public class ZAuctionManager extends ZUtils implements AuctionManager {
private final AuctionExpireService auctionExpireService;

private final Map<Player, PlayerCache> caches = new HashMap<>();
private final Map<StorageType, List<Item>> storageItems = new HashMap<>();
private final Map<StorageType, Int2ObjectMap<Item>> storageItemsById = new EnumMap<>(StorageType.class);
private final Map<UUID, IntList> idsListedByOwner = new HashMap<>();
private final Map<UUID, IntList> idsExpiredByOwner = new HashMap<>();
private final Map<UUID, IntList> idsPurchasedByBuyer = new HashMap<>();

public ZAuctionManager(AuctionPlugin plugin) {
this.plugin = plugin;
Expand All @@ -59,7 +68,7 @@ public ZAuctionManager(AuctionPlugin plugin) {
this.auctionExpireService = new ExpireService(plugin, this);

for (StorageType value : StorageType.values()) {
this.storageItems.put(value, new ArrayList<>());
this.storageItemsById.put(value, new Int2ObjectOpenHashMap<>());
}
}

Expand Down Expand Up @@ -113,92 +122,183 @@ public AuctionExpireService getExpireService() {

@Override
public List<Item> getItems(StorageType storageType) {
return this.storageItems.get(storageType);
return new ArrayList<>(this.storageItemsById.getOrDefault(storageType, Int2ObjectMaps.emptyMap()).values());
}

@Override
public List<Item> getItems(StorageType storageType, Predicate<Item> predicate) {
List<Item> items = new ArrayList<>();

var iterator = getItems(storageType).iterator();
while (iterator.hasNext()) {

var item = iterator.next();
if (item.isExpired()) {
this.auctionExpireService.processExpiredItem(item, storageType);
iterator.remove();
continue;
}

if (predicate.test(item)) {
items.add(item);
}
}
return items;
return resolveItems(storageType, getItemIds(storageType, predicate, null));
}

@Override
public List<Item> getItems(StorageType storageType, Predicate<Item> predicate, Comparator<Item> comparator) {

List<Item> result = getItems(storageType, predicate);

if (comparator != null && result.size() > 1) {
result.sort(comparator);
}

return result;
return resolveItems(storageType, getItemIds(storageType, predicate, comparator));
}

@Override
public void addItem(StorageType storageType, Item item) {
getItems(storageType).add(item);
var storage = this.storageItemsById.get(storageType);
storage.put(item.getId(), item);
this.indexItem(storageType, item);
}

@Override
public void removeItem(StorageType storageType, Item item) {
getItems(storageType).remove(item);
removeItem(storageType, item.getId());
}

@Override
public void removeItem(StorageType storageType, int itemId) {
getItems(storageType).removeIf(item -> item.getId() == itemId);
var storage = this.storageItemsById.get(storageType);
if (storage == null) return;

Item removed = storage.remove(itemId);
if (removed != null) {
this.deindexItem(storageType, removed);
}
}

@Override
public List<Item> getItemsListedForSale(Player player) {
var cache = getCache(player);
var sort = cache.get(PlayerCacheKey.ITEM_SORT, this.plugin.getConfiguration().getSort().defaultSort());
return cache.getOrCompute(PlayerCacheKey.ITEMS_LISTED, () -> getItems(StorageType.LISTED, item -> item.getStatus() == ItemStatus.AVAILABLE, sort.getComparator()));
IntList ids = cache.getOrCompute(PlayerCacheKey.ITEMS_LISTED, () -> getItemIds(StorageType.LISTED, item -> item.getStatus() == ItemStatus.AVAILABLE, sort.getComparator()));
return resolveItems(StorageType.LISTED, ids);
}

@Override
public List<Item> getExpiredItems(Player player) {
return getCache(player).getOrCompute(PlayerCacheKey.ITEMS_EXPIRED, () -> getItems(StorageType.EXPIRED, item -> item.getSellerUniqueId().equals(player.getUniqueId()), Comparator.comparing(Item::getExpiredAt)));
IntList ids = getCache(player).getOrCompute(PlayerCacheKey.ITEMS_EXPIRED, () -> getItemIds(StorageType.EXPIRED, item -> item.getSellerUniqueId().equals(player.getUniqueId()), Comparator.comparing(Item::getExpiredAt)));
return resolveItems(StorageType.EXPIRED, ids);
}

@Override
public List<Item> getExpiredItems(UUID uniqueId) {
return getItems(StorageType.EXPIRED, item -> item.getSellerUniqueId().equals(uniqueId), Comparator.comparing(Item::getExpiredAt));
return resolveItems(StorageType.EXPIRED, getItemIds(StorageType.EXPIRED, item -> item.getSellerUniqueId().equals(uniqueId), Comparator.comparing(Item::getExpiredAt)));
}

@Override
public List<Item> getPlayerOwnedItems(Player player) {
return getCache(player).getOrCompute(PlayerCacheKey.ITEMS_OWNED, () -> getItems(StorageType.LISTED, item -> item.getSellerUniqueId().equals(player.getUniqueId()), Comparator.comparing(Item::getExpiredAt)));
IntList ids = getCache(player).getOrCompute(PlayerCacheKey.ITEMS_OWNED, () -> getItemIds(StorageType.LISTED, item -> item.getSellerUniqueId().equals(player.getUniqueId()), Comparator.comparing(Item::getExpiredAt)));
return resolveItems(StorageType.LISTED, ids);
}

@Override
public List<Item> getPlayerOwnedItems(UUID uniqueId) {
return getItems(StorageType.LISTED, item -> item.getSellerUniqueId().equals(uniqueId), Comparator.comparing(Item::getExpiredAt));
return resolveItems(StorageType.LISTED, getItemIds(StorageType.LISTED, item -> item.getSellerUniqueId().equals(uniqueId), Comparator.comparing(Item::getExpiredAt)));
}

@Override
public List<Item> getPurchasedItems(Player player) {
return getCache(player).getOrCompute(PlayerCacheKey.ITEMS_PURCHASED, () -> getItems(StorageType.PURCHASED, item -> item.getBuyerUniqueId().equals(player.getUniqueId()), Comparator.comparing(Item::getExpiredAt)));
IntList ids = getCache(player).getOrCompute(PlayerCacheKey.ITEMS_PURCHASED, () -> getItemIds(StorageType.PURCHASED, item -> item.getBuyerUniqueId() != null && item.getBuyerUniqueId().equals(player.getUniqueId()), Comparator.comparing(Item::getExpiredAt)));
return resolveItems(StorageType.PURCHASED, ids);
}

@Override
public List<Item> getPurchasedItems(UUID uniqueId) {
return getItems(StorageType.PURCHASED, item -> item.getBuyerUniqueId() != null && item.getBuyerUniqueId().equals(uniqueId), Comparator.comparing(Item::getExpiredAt));
return resolveItems(StorageType.PURCHASED, getItemIds(StorageType.PURCHASED, item -> item.getBuyerUniqueId() != null && item.getBuyerUniqueId().equals(uniqueId), Comparator.comparing(Item::getExpiredAt)));
}

@Override
public List<Item> resolveItems(StorageType storageType, IntList ids) {
if (ids == null || ids.isEmpty()) return List.of();

Int2ObjectMap<Item> storage = this.storageItemsById.get(storageType);
if (storage == null || storage.isEmpty()) return List.of();

List<Item> resolved = new ArrayList<>(ids.size());
for (int id : ids) {
Item item = storage.get(id);
if (item != null) {
resolved.add(item);
}
}

return resolved;
}

public List<Item> onPlayerOpenMenu(Player player) {
IntList ids = getCache(player).get(PlayerCacheKey.ITEMS_LISTED, new IntArrayList());
return resolveItems(StorageType.LISTED, ids);
}

private IntList getItemIds(StorageType storageType, Predicate<Item> predicate, Comparator<Item> comparator) {
Int2ObjectMap<Item> items = this.storageItemsById.get(storageType);
if (items == null || items.isEmpty()) return new IntArrayList();

List<Item> filtered = new ArrayList<>();
for (Item item : items.values()) {
if (item.isExpired()) {
this.auctionExpireService.processExpiredItem(item, storageType);
continue;
Comment on lines +230 to +233

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove expired items from storage when filtering

Expired listings are detected in getItemIds but the loop only calls processExpiredItem and never removes the expired entry from storageItemsById. Because the item stays in the LISTED bucket, every subsequent call that touches the cache re-runs the expiration side effects and the entry continues to be counted as listed (e.g., GlobalPlaceholders#listed_items reads getItems(StorageType.LISTED).size()). The previous iterator-based logic removed expired items, so this regression causes repeated expiration processing and inflated listing counts until a manual removal occurs.

Useful? React with 👍 / 👎.

}

if (predicate.test(item)) {
filtered.add(item);
}
}

if (comparator != null && filtered.size() > 1) {
filtered.sort(comparator);
}

IntList ids = new IntArrayList(filtered.size());
for (Item item : filtered) {
ids.add(item.getId());
}

return ids;
}

private void indexItem(StorageType storageType, Item item) {
Map<UUID, IntList> index = getIndexFor(storageType);
if (index == null) return;

UUID owner = getOwner(storageType, item);
addToIndex(index, owner, item.getId());
}

private void deindexItem(StorageType storageType, Item item) {
Map<UUID, IntList> index = getIndexFor(storageType);
if (index == null) return;

UUID owner = getOwner(storageType, item);
removeFromIndex(index, owner, item.getId());
}

private Map<UUID, IntList> getIndexFor(StorageType storageType) {
return switch (storageType) {
case LISTED -> this.idsListedByOwner;
case EXPIRED -> this.idsExpiredByOwner;
case PURCHASED -> this.idsPurchasedByBuyer;
default -> null;
};
}

private UUID getOwner(StorageType storageType, Item item) {
return switch (storageType) {
case LISTED, EXPIRED -> item.getSellerUniqueId();
case PURCHASED -> item.getBuyerUniqueId();
default -> null;
};
}

private void addToIndex(Map<UUID, IntList> index, UUID owner, int itemId) {
if (owner == null) return;

index.computeIfAbsent(owner, uuid -> new IntArrayList()).add(itemId);
}

private void removeFromIndex(Map<UUID, IntList> index, UUID owner, int itemId) {
if (owner == null) return;

IntList ids = index.get(owner);
if (ids == null) return;

ids.rem(itemId);
if (ids.isEmpty()) {
index.remove(owner);
}
}

@Override
Expand Down Expand Up @@ -516,8 +616,8 @@ public void updateListedItems(Item item, boolean added, Player ignoredPlayer) {

private void removeFromCache(Player player, Item item) {
if (this.caches.containsKey(player)) {
List<Item> items = this.caches.get(player).get(PlayerCacheKey.ITEMS_LISTED);
items.remove(item);
IntList items = this.caches.get(player).get(PlayerCacheKey.ITEMS_LISTED);
items.rem(item.getId());
}
}

Expand Down
Loading