diff --git a/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java b/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java index 17bfccb5a..2292ed863 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java +++ b/CommonCore/SQL/src/de/steamwar/sql/GameModeConfig.java @@ -19,6 +19,7 @@ package de.steamwar.sql; +import de.steamwar.sql.internal.SQLConfig; import lombok.ToString; import java.io.File; @@ -250,6 +251,11 @@ public final class GameModeConfig { Times = new TimesConfig(loader.with("Times")); // Arena would be here to be in config order but needs Schematic.Size and EnterStages loaded afterwards Schematic = new SchematicConfig<>(loader.with("Schematic")); + for (int i = 0; i < Schematic.Limited.size(); i++) { + if (Schematic.Limited.get(i).Materials.isEmpty()) { + SQLConfig.impl.getLogger().warning(file.getName() + ": Schematic.Limited[" + i + "] matches no blocks, check its 'Materials' entry"); + } + } GameName = loader.getString("GameName", defaultGameName.apply(file)); ActiveMonths = loader.getIntList("ActiveMonths"); TeamChatPrefix = loader.getString("TeamChatPrefix", "+"); @@ -703,7 +709,10 @@ public final class GameModeConfig { public final double MaxDesignBlastResistance; /** - * List of limited block materials + * List of limited block materials
+ * Every inventory block is counted towards exactly one rule: a filled block towards a rule whose + * {@link BlockRule#Content} allows all of its items, an empty block towards a rule without + * {@link BlockRule#Content}. Empty inventory blocks without such a rule are unlimited. * * @implSpec {@link SQLWrapper#getInventoryMaterials)} are all disallowed by default to have any content but are unlimited in number. * @implSpec Any Block that has more Blast Resistance than {@link #MaxBlastResistance} will be defaulted to 0. @@ -759,18 +768,13 @@ public final class GameModeConfig { } public boolean isInventory(M material) { - return Limited.stream() - .filter(rule -> rule.Materials.contains(material)) - .anyMatch(rule -> rule.Content.loaded); + return SQLWrapper.impl.getInventoryMaterials().contains(material) || Limited.stream().anyMatch(rule -> rule.Content.loaded && rule.Materials.contains(material)); } - public Integer getItemAmount(M material, M item) { + public List> getRules(M material) { return Limited.stream() .filter(rule -> rule.Materials.contains(material)) - .filter(rule -> rule.Content.Items.contains(item)) - .mapToInt(rule -> rule.Content.Amount) - .max() - .orElse(0); + .collect(Collectors.toList()); } @ToString diff --git a/CommonCore/SQL/src/de/steamwar/sql/SQLWrapper.java b/CommonCore/SQL/src/de/steamwar/sql/SQLWrapper.java index 565687fe7..47a6f0a67 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/SQLWrapper.java +++ b/CommonCore/SQL/src/de/steamwar/sql/SQLWrapper.java @@ -46,7 +46,7 @@ public interface SQLWrapper { } /** - * Every inventory material - the {@code all} group usable in {@code GameModeConfig}'s + * Every inventory material - the {@code inventory} group usable in {@code GameModeConfig}'s * {@code Limited}. Expected to be a superset of {@link #getStorageMaterials()}. */ default Set getInventoryMaterials() { diff --git a/SchematicSystem/src/SchematicSystem.properties b/SchematicSystem/src/SchematicSystem.properties index c72bcf961..adb68d095 100644 --- a/SchematicSystem/src/SchematicSystem.properties +++ b/SchematicSystem/src/SchematicSystem.properties @@ -262,9 +262,8 @@ AUTO_CHECKER_RESULT_BLOCKS = §7Blocks: §c{0}§7, Max: §e{1} AUTO_CHECKER_RESULT_UNKNOWN_MATERIAL = §7Unknown block: §c{0} AUTO_CHECKER_RESULT_TOO_MANY_BLOCK = §7{0}: §c{1}§7, Max: §e{2} AUTO_CHECKER_RESULT_FORBIDDEN_BLOCK = §7Forbidden block: §c{0} -AUTO_CHECKER_RESULT_WIND_CHARGES = §7Windcharges: §c{0}§7, Max: §e2048 -AUTO_CHECKER_RESULT_WIND_CHARGES_DISPENSER = §7Dispenser: §c[{0}, {1}, {2}]§7, Windcharges: §c{3}§7 AUTO_CHECKER_RESULT_FORBIDDEN_ITEM = §7Forbidden Item: [{0}, {1}, {2}] -> §c{3} +AUTO_CHECKER_RESULT_MIXED_ITEMS = §7Items of different rules in one inventory: [{0}, {1}, {2}] -> §c{3} AUTO_CHECKER_RESULT_DEFUNCT_NBT = §7Defunct NBT: §7[{0}, {1}, {2}] AUTO_CHECKER_RESULT_DESIGN_BLOCK = §7{0} in Design: [{1}, {2}, {3}] AUTO_CHECKER_RESULT_ENTITY = §7Entity: §7[{0}, {1}, {2}] diff --git a/SchematicSystem/src/SchematicSystem_de.properties b/SchematicSystem/src/SchematicSystem_de.properties index d584c602c..6d54d9bc1 100644 --- a/SchematicSystem/src/SchematicSystem_de.properties +++ b/SchematicSystem/src/SchematicSystem_de.properties @@ -242,9 +242,8 @@ AUTO_CHECKER_RESULT_BLOCKS = §7Blöcke: §c{0}§7, Max: §e{1} AUTO_CHECKER_RESULT_UNKNOWN_MATERIAL = §7Unbekannter Block: §c{0} AUTO_CHECKER_RESULT_TOO_MANY_BLOCK = §7{0}: §c{1}§7, Max: §e{2} AUTO_CHECKER_RESULT_FORBIDDEN_BLOCK = §7Verbotener Block: §c{0} -AUTO_CHECKER_RESULT_WIND_CHARGES = §7Windcharges: §c{0}§7, Max: §e2048 -AUTO_CHECKER_RESULT_WIND_CHARGES_DISPENSER = §7Werfer: §c[{0}, {1}, {2}]§7, Windcharges: §c{3}§7 AUTO_CHECKER_RESULT_FORBIDDEN_ITEM = §7Verbotener gegenstand: [{0}, {1}, {2}] -> §c{3} +AUTO_CHECKER_RESULT_MIXED_ITEMS = §7Gegenstände verschiedener Regeln in einem Inventar: [{0}, {1}, {2}] -> §c{3} AUTO_CHECKER_RESULT_DEFUNCT_NBT = §7Keine NBT-Daten: §c[{0}, {1}, {2}] AUTO_CHECKER_RESULT_DESIGN_BLOCK = §7{0} im Design: [{1}, {2}, {3}] AUTO_CHECKER_RESULT_RECORD = §7Schallplatte: §c[{0}, {1}, {2}] diff --git a/SchematicSystem/src/de/steamwar/schematicsystem/autocheck/AutoChecker.java b/SchematicSystem/src/de/steamwar/schematicsystem/autocheck/AutoChecker.java index cb23bbbdc..1d0b6bc7b 100644 --- a/SchematicSystem/src/de/steamwar/schematicsystem/autocheck/AutoChecker.java +++ b/SchematicSystem/src/de/steamwar/schematicsystem/autocheck/AutoChecker.java @@ -25,6 +25,7 @@ import com.sk89q.worldedit.extent.clipboard.Clipboard; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.world.block.BaseBlock; import de.steamwar.sql.GameModeConfig; +import de.steamwar.sql.GameModeConfig.SchematicConfig.BlockRule; import lombok.Getter; import lombok.ToString; import org.bukkit.Material; @@ -62,6 +63,7 @@ public class AutoChecker { public AutoChecker.BlockScanResult scan(Clipboard clipboard, GameModeConfig type) { AutoChecker.BlockScanResult result = new AutoChecker.BlockScanResult(); + Map>> ruleCandidates = new HashMap<>(); BlockVector3 min = clipboard.getMinimumPoint(); BlockVector3 max = clipboard.getMaximumPoint(); for (int x = min.x(); x <= max.x(); x++) { @@ -75,13 +77,13 @@ public class AutoChecker { BlockPos pos = new BlockPos(x, y, z); + result.getBlockCounts().merge(material, 1, Integer::sum); if (type.Schematic.isInventory(material)) { - checkInventory(result, block, material, pos, type); - if (!result.getInventoryItemCounts().getOrDefault(pos, Collections.emptyMap()).isEmpty()) { - result.getBlockCounts().merge(material, 1, Integer::sum); + result.getInventoryBlockType().put(pos, material); + List> candidates = checkInventory(result, block, material, pos, type); + if (!candidates.isEmpty()) { + ruleCandidates.put(pos, candidates); } - } else { - result.getBlockCounts().merge(material, 1, Integer::sum); } if (x == min.x() || x == max.x() || y == max.y() || z == min.z() || z == max.z()) { @@ -90,28 +92,28 @@ public class AutoChecker { } } } + assignRules(result, ruleCandidates); return result; } - private void checkInventory(AutoChecker.BlockScanResult result, BaseBlock block, Material material, BlockPos pos, GameModeConfig type) { + /** + * Scans the inventory and returns the rules this block may be counted towards. + * An empty list means the block is either faulty (reported in the result) or unlimited. + */ + private List> checkInventory(AutoChecker.BlockScanResult result, BaseBlock block, Material material, BlockPos pos, GameModeConfig type) { CompoundTag nbt = block.getNbtData(); if (nbt == null) { result.getDefunctNbt().add(pos); - return; + return Collections.emptyList(); } - if (material == Material.JUKEBOX && nbt.getValue().containsKey("RecordItem")) { result.getRecords().add(pos); - return; + return Collections.emptyList(); } - List items = nbt.getList("Items", CompoundTag.class); - if (items.isEmpty()) return; // Leeres Inventar - Map itemCounts = new EnumMap<>(Material.class); - int windChargeCount = 0; - for (CompoundTag item : items) { + for (CompoundTag item : nbt.getList("Items", CompoundTag.class)) { if (!item.containsKey("id")) { result.getDefunctNbt().add(pos); continue; @@ -121,22 +123,69 @@ public class AutoChecker { if (itemType == null) // Leere Slots continue; - if (type.Schematic.Type.getName().equals("wargearseason26") && material == Material.DISPENSER && itemType == Material.WIND_CHARGE) { - windChargeCount += item.getInt("count"); - } else if (type.Schematic.getItemAmount(material, itemType) == 0) { - result.getForbiddenItems().computeIfAbsent(pos, blockVector3 -> new HashSet<>()).add(itemType); - } else { - itemCounts.merge(itemType, item.getInt("count"), Integer::sum); - } + itemCounts.merge(itemType, item.getInt("count"), Integer::sum); if (item.containsKey("tag")) { result.getForbiddenNbt().computeIfAbsent(pos, blockVector3 -> new HashSet<>()).add(itemType); } } - if (!itemCounts.isEmpty()) { - result.getInventoryItemCounts().put(pos, itemCounts); - result.getInventoryBlockType().put(pos, material); + + List> rules = type.Schematic.getRules(material); + if (itemCounts.isEmpty()) { // Leeres Inventar + return rules.stream().filter(rule -> !rule.Content.loaded).collect(Collectors.toList()); } - result.getWindChargeCount().put(pos, windChargeCount); + result.getInventoryItemCounts().put(pos, itemCounts); + + List> contentRules = rules.stream().filter(rule -> rule.Content.loaded).collect(Collectors.toList()); + List> itemsFitting = contentRules.stream() + .filter(rule -> rule.Content.Items.containsAll(itemCounts.keySet())) + .collect(Collectors.toList()); + List> fitting = itemsFitting.stream() + .filter(rule -> itemCounts.values().stream().allMatch(count -> count <= rule.Content.Amount)) + .collect(Collectors.toList()); + if (!fitting.isEmpty()) { + return fitting; + } + + if (!itemsFitting.isEmpty()) { + BlockRule best = Collections.max(itemsFitting, Comparator.comparingInt(rule -> rule.Content.Amount)); + itemCounts.forEach((itemType, count) -> { + if (count > best.Content.Amount) { + result.getTooManyItems().computeIfAbsent(pos, blockVector3 -> new EnumMap<>(Material.class)).put(itemType, best.Content.Amount); + } + }); + return Collections.singletonList(best); + } + + Set allowed = contentRules.stream().flatMap(rule -> rule.Content.Items.stream()).collect(Collectors.toSet()); + Set forbidden = itemCounts.keySet().stream().filter(itemType -> !allowed.contains(itemType)).collect(Collectors.toSet()); + if (!forbidden.isEmpty()) { + result.getForbiddenItems().put(pos, forbidden); + } + + Set rest = itemCounts.keySet().stream().filter(allowed::contains).collect(Collectors.toSet()); + if (!rest.isEmpty()) { + // Every remaining item is allowed on its own, but no single rule allows all of them together + BlockRule best = Collections.max(contentRules, Comparator.comparingLong(rule -> rest.stream().filter(rule.Content.Items::contains).count())); + Set mixed = rest.stream().filter(itemType -> !best.Content.Items.contains(itemType)).collect(Collectors.toSet()); + if (!mixed.isEmpty()) { + result.getMixedItems().put(pos, mixed); + } + } + return Collections.emptyList(); + } + + /** + * Counts every block towards one of its candidate rules, preferring the rule with the most capacity left. + * Blocks with fewer candidates are assigned first. + */ + private void assignRules(AutoChecker.BlockScanResult result, Map>> ruleCandidates) { + ruleCandidates.entrySet().stream() + .sorted(Comparator.comparingInt(entry -> entry.getValue().size())) + .forEach(entry -> { + BlockRule rule = Collections.max(entry.getValue(), Comparator.comparingLong(candidate -> (long) candidate.Amount - result.getRuleCounts().getOrDefault(candidate, 0))); + result.getRuleCounts().merge(rule, 1, Integer::sum); + result.getRuleBlockTypes().computeIfAbsent(rule, r -> EnumSet.noneOf(Material.class)).add(result.getInventoryBlockType().get(entry.getKey())); + }); } @Getter @@ -148,8 +197,11 @@ public class AutoChecker { private final Map> designBlocks = new EnumMap<>(Material.class); private final Map> inventoryItemCounts = new HashMap<>(); private final Map inventoryBlockType = new HashMap<>(); - private final Map windChargeCount = new HashMap<>(); + private final Map, Integer> ruleCounts = new HashMap<>(); + private final Map, Set> ruleBlockTypes = new HashMap<>(); + private final Map> tooManyItems = new HashMap<>(); private final Map> forbiddenItems = new HashMap<>(); + private final Map> mixedItems = new HashMap<>(); private final Map> forbiddenNbt = new HashMap<>(); } } diff --git a/SchematicSystem/src/de/steamwar/schematicsystem/autocheck/AutoCheckerResult.java b/SchematicSystem/src/de/steamwar/schematicsystem/autocheck/AutoCheckerResult.java index c59f17aba..ca6842449 100644 --- a/SchematicSystem/src/de/steamwar/schematicsystem/autocheck/AutoCheckerResult.java +++ b/SchematicSystem/src/de/steamwar/schematicsystem/autocheck/AutoCheckerResult.java @@ -31,6 +31,7 @@ import org.bukkit.entity.Player; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; @Builder @Getter @@ -46,12 +47,12 @@ public class AutoCheckerResult { public boolean isOk() { return blockScanResult.getRecords().isEmpty() && blockScanResult.getForbiddenItems().isEmpty() && + blockScanResult.getMixedItems().isEmpty() && blockScanResult.getForbiddenNbt().isEmpty() && isSizeOk() && isBlockCountOk() && isLimitedBlocksOK() && isInventoryItemsOK() && - isWindchargeCountOK() && !type.isAfterDeadline() && entities.isEmpty() && isDesignBlastResistanceOK(); @@ -62,23 +63,8 @@ public class AutoCheckerResult { !type.isAfterDeadline(); } - public boolean isWindchargeCountOK() { - if (type.Schematic.Type.getName().equals("wargearseason26")) { - int windChargesCount = blockScanResult.getWindChargeCount().values().stream().reduce(Integer::sum).orElse(0); - return windChargesCount <= 2048; - } else { - return true; - } - } - public boolean isInventoryItemsOK() { - return blockScanResult.getInventoryItemCounts().entrySet().stream().allMatch(posEntry -> { - Material inventory = blockScanResult.getInventoryBlockType().get(posEntry.getKey()); - return posEntry.getValue().entrySet().stream().allMatch(itemEntry -> { - int cap = type.Schematic.getItemAmount(inventory, itemEntry.getKey()); - return itemEntry.getValue() <= cap; - }); - }); + return blockScanResult.getTooManyItems().isEmpty(); } public boolean hasWarnings() { @@ -107,7 +93,10 @@ public class AutoCheckerResult { public boolean isLimitedBlocksOK() { return blockScanResult.getBlockCounts().entrySet().stream() - .allMatch(entry -> entry.getValue() <= type.Schematic.getMaxCount(entry.getKey())); + .filter(entry -> !type.Schematic.isInventory(entry.getKey())) + .allMatch(entry -> entry.getValue() <= type.Schematic.getMaxCount(entry.getKey())) && + blockScanResult.getRuleCounts().entrySet().stream() + .allMatch(entry -> entry.getValue() <= entry.getKey().Amount); } public boolean isDesignBlastResistanceOK() { @@ -132,6 +121,7 @@ public class AutoCheckerResult { } if (!isLimitedBlocksOK()) { blockScanResult.getBlockCounts().forEach((mat, count) -> { + if (type.Schematic.isInventory(mat)) return; int maxCount = type.Schematic.getMaxCount(mat); if (count > maxCount) { if (maxCount == 0) { @@ -141,30 +131,25 @@ public class AutoCheckerResult { } } }); - } - - if (!isWindchargeCountOK()) { - int windChargesCount = blockScanResult.getWindChargeCount().values().stream().reduce(Integer::sum).orElse(0); - SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_WIND_CHARGES", p, windChargesCount, 2048); - blockScanResult.getWindChargeCount().entrySet().stream().filter(blockVector3IntegerEntry -> blockVector3IntegerEntry.getValue() > 0).forEach(blockVector3IntegerEntry -> { - SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_WIND_CHARGES_DISPENSER", p, SchematicSystem.MESSAGE.parse("AUTO_CHECKER_RESULT_TELEPORT_HERE", p), tpCommandTo(blockVector3IntegerEntry.getKey()), - blockVector3IntegerEntry.getKey().getBlockX(), - blockVector3IntegerEntry.getKey().getBlockY(), - blockVector3IntegerEntry.getKey().getBlockZ(), - blockVector3IntegerEntry.getValue()); + blockScanResult.getRuleCounts().forEach((rule, count) -> { + if (count <= rule.Amount) return; + String blocks = blockScanResult.getRuleBlockTypes().get(rule).stream().map(Material::name).collect(Collectors.joining(", ")); + if (rule.Amount == 0) { + SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_FORBIDDEN_BLOCK", p, blocks); + } else { + SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_TOO_MANY_BLOCK", p, blocks, count, rule.Amount); + } }); } - blockScanResult.getInventoryItemCounts().forEach((pos, itemCounts) -> { + blockScanResult.getTooManyItems().forEach((pos, caps) -> { Material inventory = blockScanResult.getInventoryBlockType().get(pos); - itemCounts.forEach((itemType, count) -> { - int cap = type.Schematic.getItemAmount(inventory, itemType); - if (count <= cap) return; + caps.forEach((itemType, cap) -> { SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_TOO_MANY_INVENTORY_ITEMS", p, SchematicSystem.MESSAGE.parse("AUTO_CHECKER_RESULT_TELEPORT_HERE", p), tpCommandTo(pos), pos.getBlockX(), pos.getBlockY(), pos.getBlockZ(), - count, + blockScanResult.getInventoryItemCounts().get(pos).get(itemType), cap, itemType.name(), inventory.name()); @@ -177,6 +162,9 @@ public class AutoCheckerResult { blockScanResult.getForbiddenItems().forEach((blockVector3, materials) -> { SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_FORBIDDEN_ITEM", p, SchematicSystem.MESSAGE.parse("AUTO_CHECKER_RESULT_TELEPORT_HERE", p), tpCommandTo(blockVector3), blockVector3.getX(), blockVector3.getY(), blockVector3.getZ(), setToString(materials)); }); + blockScanResult.getMixedItems().forEach((blockVector3, materials) -> { + SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_MIXED_ITEMS", p, SchematicSystem.MESSAGE.parse("AUTO_CHECKER_RESULT_TELEPORT_HERE", p), tpCommandTo(blockVector3), blockVector3.getX(), blockVector3.getY(), blockVector3.getZ(), setToString(materials)); + }); blockScanResult.getForbiddenNbt().forEach((blockVector3, materials) -> { SchematicSystem.MESSAGE.sendPrefixless("AUTO_CHECKER_RESULT_FORBIDDEN_ITEM_NBT", p, SchematicSystem.MESSAGE.parse("AUTO_CHECKER_RESULT_TELEPORT_HERE", p), tpCommandTo(blockVector3), blockVector3.getX(), blockVector3.getY(), blockVector3.getZ(), setToString(materials)); }); diff --git a/SchematicSystem/src/de/steamwar/schematicsystem/commands/schematiccommand/SchematicCommand.java b/SchematicSystem/src/de/steamwar/schematicsystem/commands/schematiccommand/SchematicCommand.java index e0b0b2ce9..17525d8a7 100644 --- a/SchematicSystem/src/de/steamwar/schematicsystem/commands/schematiccommand/SchematicCommand.java +++ b/SchematicSystem/src/de/steamwar/schematicsystem/commands/schematiccommand/SchematicCommand.java @@ -48,6 +48,7 @@ import java.util.*; import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import static de.steamwar.schematicsystem.commands.schematiccommand.SchematicCommandHelp.*; import static de.steamwar.schematicsystem.commands.schematiccommand.SchematicCommandUtils.togglePublic; @@ -171,8 +172,8 @@ public class SchematicCommand extends SWCommand { Map> toBeCheckedInvs = new HashMap<>(); - toBeCheckedInvs.putAll(result.getBlockScanResult().getForbiddenItems()); - toBeCheckedInvs.putAll(result.getBlockScanResult().getForbiddenNbt()); + Stream.of(result.getBlockScanResult().getForbiddenItems(), result.getBlockScanResult().getMixedItems(), result.getBlockScanResult().getForbiddenNbt()) + .forEach(invs -> invs.forEach((pos, materials) -> toBeCheckedInvs.computeIfAbsent(pos, p -> new HashSet<>()).addAll(materials))); for (Map.Entry> entry : toBeCheckedInvs.entrySet()) { BlockPos pos = entry.getKey(); @@ -197,16 +198,13 @@ public class SchematicCommand extends SWCommand { clipboard.setBlock(vector, block.toBaseBlock(builder.build())); } - for (Map.Entry> posEntry : result.getBlockScanResult().getInventoryItemCounts().entrySet()) { + for (Map.Entry> posEntry : result.getBlockScanResult().getTooManyItems().entrySet()) { BlockPos pos = posEntry.getKey(); - Material inventory = result.getBlockScanResult().getInventoryBlockType().get(pos); + Map itemCounts = result.getBlockScanResult().getInventoryItemCounts().get(pos); for (Map.Entry itemEntry : posEntry.getValue().entrySet()) { Material itemType = itemEntry.getKey(); - Integer cap = type.Schematic.getItemAmount(inventory, itemType); - if (itemEntry.getValue() <= cap) { - continue; - } + int cap = itemEntry.getValue(); BlockVector3 vector = BlockVector3.at(pos.getX(), pos.getY(), pos.getZ()); BaseBlock block = clipboard.getFullBlock(vector); @@ -215,7 +213,7 @@ public class SchematicCommand extends SWCommand { List items = new ArrayList<>(tag.getList("Items", CompoundTag.class)); Collections.reverse(items); // To let the first item be in the inventory List list = new ArrayList<>(); - int diff = itemEntry.getValue() - cap; + int diff = itemCounts.get(itemType) - cap; for (CompoundTag item : items) { if (item == null) { continue;