/*
* 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<>();
}
}