/* * This file is a part of the SteamWar software. * * Copyright (C) 2025 SteamWar.de-Serverteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package de.steamwar.schematicsystem.autocheck; import com.sk89q.jnbt.CompoundTag; import com.sk89q.worldedit.entity.Entity; 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; import java.util.*; import java.util.stream.Collectors; public class AutoChecker { public static final AutoChecker impl = new AutoChecker(); public AutoCheckerResult check(Clipboard clipboard, GameModeConfig type) { return AutoCheckerResult.builder() .type(type) .height(clipboard.getDimensions().y()) .width(clipboard.getDimensions().x()) .depth(clipboard.getDimensions().z()) .blockScanResult(scan(clipboard, type)) .entities( clipboard.getEntities().stream() .map(Entity::getLocation) .map(blockVector3 -> new BlockPos(blockVector3.getBlockX(), blockVector3.getBlockY(), blockVector3.getBlockZ())) .collect(Collectors.toList())) .build(); } public AutoCheckerResult sizeCheck(Clipboard clipboard, GameModeConfig type) { return AutoCheckerResult.builder() .type(type) .height(clipboard.getDimensions().y()) .width(clipboard.getDimensions().x()) .depth(clipboard.getDimensions().z()) .build(); } 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++) { for (int y = min.y(); y <= max.y(); y++) { for (int z = min.z(); z <= max.z(); z++) { final BaseBlock block = clipboard.getFullBlock(BlockVector3.at(x, y, z)); final Material material = Material.matchMaterial(block.getBlockType().id()); if (material == null) { continue; } BlockPos pos = new BlockPos(x, y, z); result.getBlockCounts().merge(material, 1, Integer::sum); if (type.Schematic.isInventory(material)) { result.getInventoryBlockType().put(pos, material); List> candidates = checkInventory(result, block, material, pos, type); if (!candidates.isEmpty()) { ruleCandidates.put(pos, candidates); } } if (x == min.x() || x == max.x() || y == max.y() || z == min.z() || z == max.z()) { result.getDesignBlocks().computeIfAbsent(material, m -> new ArrayList<>()).add(pos); } } } } assignRules(result, ruleCandidates); return result; } /** * 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 Collections.emptyList(); } if (material == Material.JUKEBOX && nbt.getValue().containsKey("RecordItem")) { result.getRecords().add(pos); return Collections.emptyList(); } Map itemCounts = new EnumMap<>(Material.class); for (CompoundTag item : nbt.getList("Items", CompoundTag.class)) { if (!item.containsKey("id")) { result.getDefunctNbt().add(pos); continue; } Material itemType = Material.matchMaterial(item.getString("id")); if (itemType == null) // Leere Slots continue; itemCounts.merge(itemType, item.getInt("count"), Integer::sum); if (item.containsKey("tag")) { result.getForbiddenNbt().computeIfAbsent(pos, blockVector3 -> new HashSet<>()).add(itemType); } } List> rules = type.Schematic.getRules(material); if (itemCounts.isEmpty()) { // Leeres Inventar return rules.stream().filter(rule -> !rule.Content.loaded).collect(Collectors.toList()); } 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 @ToString public static class BlockScanResult { private final Map blockCounts = new EnumMap<>(Material.class); private final List defunctNbt = new ArrayList<>(); private final List records = new ArrayList<>(); private final Map> designBlocks = new EnumMap<>(Material.class); private final Map> inventoryItemCounts = new HashMap<>(); private final Map inventoryBlockType = 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<>(); } }