Fix AutoChecker inventory rules

Count every inventory block towards exactly one Schematic.Limited rule:
filled blocks towards a rule whose Content allows all their items, empty
blocks towards a rule without Content (unlimited if there is none). Each
rule's Amount now caps how many blocks it covers, instead of taking the
highest Amount of all rules for that block type.

Inventory blocks whose items fit no rule are now checked and reported,
items from different rules in one block are reported as mixed, and the
auto-fix trims or removes them accordingly.

Remove the hardcoded wargearseason26 wind charge handling and its 2048
limit, wind charges are configured via Limited rules now.

Log a warning for Limited rules that match no blocks, e.g. a misspelled
'Materials' key.

Fixes #458, #481, #502
This commit is contained in:
2026-10-15 17:53:06 +02:00
parent 2b42f0d13c
commit d727982ce0
7 changed files with 123 additions and 83 deletions
@@ -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<Material, String> type) {
AutoChecker.BlockScanResult result = new AutoChecker.BlockScanResult();
Map<BlockPos, List<BlockRule<Material>>> 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<BlockRule<Material>> 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<Material, String> 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<BlockRule<Material>> checkInventory(AutoChecker.BlockScanResult result, BaseBlock block, Material material, BlockPos pos, GameModeConfig<Material, String> 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<CompoundTag> items = nbt.getList("Items", CompoundTag.class);
if (items.isEmpty()) return; // Leeres Inventar
Map<Material, Integer> 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<BlockRule<Material>> 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<BlockRule<Material>> contentRules = rules.stream().filter(rule -> rule.Content.loaded).collect(Collectors.toList());
List<BlockRule<Material>> itemsFitting = contentRules.stream()
.filter(rule -> rule.Content.Items.containsAll(itemCounts.keySet()))
.collect(Collectors.toList());
List<BlockRule<Material>> 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<Material> 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<Material> allowed = contentRules.stream().flatMap(rule -> rule.Content.Items.stream()).collect(Collectors.toSet());
Set<Material> forbidden = itemCounts.keySet().stream().filter(itemType -> !allowed.contains(itemType)).collect(Collectors.toSet());
if (!forbidden.isEmpty()) {
result.getForbiddenItems().put(pos, forbidden);
}
Set<Material> 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<Material> best = Collections.max(contentRules, Comparator.comparingLong(rule -> rest.stream().filter(rule.Content.Items::contains).count()));
Set<Material> 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<BlockPos, List<BlockRule<Material>>> ruleCandidates) {
ruleCandidates.entrySet().stream()
.sorted(Comparator.comparingInt(entry -> entry.getValue().size()))
.forEach(entry -> {
BlockRule<Material> 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<Material, List<BlockPos>> designBlocks = new EnumMap<>(Material.class);
private final Map<BlockPos, Map<Material, Integer>> inventoryItemCounts = new HashMap<>();
private final Map<BlockPos, Material> inventoryBlockType = new HashMap<>();
private final Map<BlockPos, Integer> windChargeCount = new HashMap<>();
private final Map<BlockRule<Material>, Integer> ruleCounts = new HashMap<>();
private final Map<BlockRule<Material>, Set<Material>> ruleBlockTypes = new HashMap<>();
private final Map<BlockPos, Map<Material, Integer>> tooManyItems = new HashMap<>();
private final Map<BlockPos, Set<Material>> forbiddenItems = new HashMap<>();
private final Map<BlockPos, Set<Material>> mixedItems = new HashMap<>();
private final Map<BlockPos, Set<Material>> forbiddenNbt = new HashMap<>();
}
}