diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/command/feature/DebugCleanCacheCommand.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/command/feature/DebugCleanCacheCommand.java index 8337140f6..d6b01bcd3 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/command/feature/DebugCleanCacheCommand.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/command/feature/DebugCleanCacheCommand.java @@ -1,14 +1,22 @@ package net.momirealms.craftengine.bukkit.plugin.command.feature; -import net.momirealms.craftengine.bukkit.api.CraftEngineItems; +import net.momirealms.craftengine.bukkit.block.BukkitBlockManager; import net.momirealms.craftengine.bukkit.font.BukkitFontManager; import net.momirealms.craftengine.bukkit.item.BukkitItemManager; import net.momirealms.craftengine.bukkit.plugin.command.BukkitCommandFeature; +import net.momirealms.craftengine.core.block.BlockStateWrapper; +import net.momirealms.craftengine.core.block.CustomBlock; +import net.momirealms.craftengine.core.block.ImmutableBlockState; +import net.momirealms.craftengine.core.font.BitmapImage; +import net.momirealms.craftengine.core.item.CustomItem; import net.momirealms.craftengine.core.pack.allocator.IdAllocator; +import net.momirealms.craftengine.core.pack.allocator.VisualBlockStateAllocator; import net.momirealms.craftengine.core.plugin.CraftEngine; import net.momirealms.craftengine.core.plugin.command.CraftEngineCommandManager; +import net.momirealms.craftengine.core.util.FileUtils; import net.momirealms.craftengine.core.util.Key; import org.bukkit.command.CommandSender; +import org.bukkit.inventory.ItemStack; import org.checkerframework.checker.nullness.qual.NonNull; import org.incendo.cloud.Command; import org.incendo.cloud.context.CommandContext; @@ -18,11 +26,11 @@ import org.incendo.cloud.suggestion.Suggestion; import org.incendo.cloud.suggestion.SuggestionProvider; import java.io.IOException; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; +import java.util.stream.Stream; public class DebugCleanCacheCommand extends BukkitCommandFeature { @@ -48,9 +56,14 @@ public class DebugCleanCacheCommand extends BukkitCommandFeature switch (type) { case "custom-model-data" -> { BukkitItemManager instance = BukkitItemManager.instance(); - Set ids = CraftEngineItems.loadedItems().keySet().stream().map(Key::toString).collect(Collectors.toSet()); + Map> idsMap = new HashMap<>(); + for (CustomItem item : instance.loadedItems().values()) { + Set ids = idsMap.computeIfAbsent(item.clientBoundMaterial(), k -> new HashSet<>()); + ids.add(item.id().asString()); + } int total = 0; - for (Map.Entry entry : instance.itemParser().idAllocators().entrySet()) { + for (Map.Entry entry : getAllCachedCustomModelData().entrySet()) { + Set ids = idsMap.getOrDefault(entry.getKey(), Collections.emptySet()); List removed = entry.getValue().cleanupUnusedIds(i -> !ids.contains(i)); total += removed.size(); try { @@ -60,18 +73,82 @@ public class DebugCleanCacheCommand extends BukkitCommandFeature return; } for (String id : removed) { - this.plugin().logger().info("Cleaned unused item: " + id); + this.plugin().logger().info("Cleaned unsued item: " + id); } } context.sender().sendMessage("Cleaned " + total + " unused custom model data"); } - case "custom-block-states" -> { - } - case "visual-block-states" -> { - } case "font", "images" -> { BukkitFontManager instance = this.plugin().fontManager(); - + Map> idsMap = new HashMap<>(); + for (BitmapImage image : instance.loadedImages().values()) { + Set ids = idsMap.computeIfAbsent(image.font(), k -> new HashSet<>()); + String id = image.id().toString(); + ids.add(id); + for (int i = 0; i < image.rows(); i++) { + for (int j = 0; j < image.columns(); j++) { + String imageArgs = id + ":" + i + ":" + j; + ids.add(imageArgs); + } + } + } + int total = 0; + for (Map.Entry entry : getAllCachedFont().entrySet()) { + Key font = entry.getKey(); + Set ids = idsMap.getOrDefault(font, Collections.emptySet()); + List removed = entry.getValue().cleanupUnusedIds(i -> !ids.contains(i)); + try { + entry.getValue().saveToCache(); + } catch (IOException e) { + this.plugin().logger().warn("Error while saving codepoint allocation for font " + font.asString(), e); + return; + } + for (String id : removed) { + this.plugin().logger().info("Cleaned unsued image: " + id); + } + total += removed.size(); + } + context.sender().sendMessage("Cleaned " + total + " unused codepoints"); + } + case "custom-block-states" -> { + BukkitBlockManager instance = BukkitBlockManager.instance(); + Set ids = new HashSet<>(); + for (CustomBlock customBlock : instance.loadedBlocks().values()) { + for (ImmutableBlockState state : customBlock.variantProvider().states()) { + ids.add(state.toString()); + } + } + IdAllocator idAllocator = instance.blockParser().internalIdAllocator(); + List removed = idAllocator.cleanupUnusedIds(i -> !ids.contains(i)); + try { + idAllocator.saveToCache(); + } catch (IOException e) { + this.plugin().logger().warn("Error while saving custom block states allocation", e); + } + for (String id : removed) { + this.plugin().logger().info("Cleaned unsued block state: " + id); + } + context.sender().sendMessage("Cleaned " + removed.size() + " unused custom block states"); + } + case "visual-block-states" -> { + BukkitBlockManager instance = BukkitBlockManager.instance(); + Set ids = new HashSet<>(); + for (CustomBlock customBlock : instance.loadedBlocks().values()) { + for (ImmutableBlockState state : customBlock.variantProvider().states()) { + ids.add(state.vanillaBlockState()); + } + } + VisualBlockStateAllocator visualBlockStateAllocator = instance.blockParser().visualBlockStateAllocator(); + List removed = visualBlockStateAllocator.cleanupUnusedIds(i -> !ids.contains(i)); + try { + visualBlockStateAllocator.saveToCache(); + } catch (IOException e) { + this.plugin().logger().warn("Error while saving visual block states allocation", e); + } + for (String id : removed) { + this.plugin().logger().info("Cleaned unsued block appearance: " + id); + } + context.sender().sendMessage("Cleaned " + removed.size() + " unused block state appearances"); } } }); @@ -81,4 +158,71 @@ public class DebugCleanCacheCommand extends BukkitCommandFeature public String getFeatureID() { return "debug_clean_cache"; } + + public Map getAllCachedCustomModelData() { + Path cacheDir = CraftEngine.instance().dataFolderPath().resolve("cache").resolve("custom-model-data"); + + Map idAllocators = new HashMap<>(); + try (Stream files = Files.list(cacheDir)) { + files.filter(this::isJsonFile) + .forEach(file -> processIdAllocatorFile(cacheDir, file, idAllocators)); + + } catch (IOException e) { + CraftEngine.instance().logger().warn("Failed to process: " + cacheDir.getFileName(), e); + } + + return idAllocators; + } + + public Map getAllCachedFont() { + Path cacheDir = CraftEngine.instance().dataFolderPath().resolve("cache").resolve("font"); + + try { + List namespaces = FileUtils.collectNamespaces(cacheDir); + Map idAllocators = new HashMap<>(); + + for (Path namespace : namespaces) { + processNamespace(namespace, idAllocators); + } + return idAllocators; + + } catch (IOException e) { + CraftEngine.instance().logger().warn("Failed to load cached id allocators from: " + cacheDir, e); + return Collections.emptyMap(); + } + } + + private void processNamespace(Path namespace, Map idAllocators) { + if (!Files.isDirectory(namespace)) { + return; + } + + try (Stream files = Files.list(namespace)) { + files.filter(this::isJsonFile) + .forEach(file -> processIdAllocatorFile(namespace, file, idAllocators)); + + } catch (IOException e) { + CraftEngine.instance().logger().warn("Failed to process namespace: " + namespace.getFileName(), e); + } + } + + private boolean isJsonFile(Path file) { + return Files.isRegularFile(file) && file.getFileName().toString().endsWith(".json"); + } + + private void processIdAllocatorFile(Path namespace, Path file, Map idAllocators) { + try { + String namespaceName = namespace.getFileName().toString(); + String fileName = FileUtils.pathWithoutExtension(file.getFileName().toString()); + + Key font = Key.of(namespaceName, fileName); + IdAllocator allocator = new IdAllocator(file); + allocator.loadFromCache(); + + idAllocators.put(font, allocator); + + } catch (Exception e) { + CraftEngine.instance().logger().warn("Failed to load id allocator from: " + file, e); + } + } } diff --git a/common-files/src/main/resources/translations/en.yml b/common-files/src/main/resources/translations/en.yml index acd02e1a4..2c0551596 100644 --- a/common-files/src/main/resources/translations/en.yml +++ b/common-files/src/main/resources/translations/en.yml @@ -278,7 +278,7 @@ warning.config.block.state.entity_renderer.model_engine.missing_model: " warning.config.block.state.variant.invalid_appearance: "Issue found in file - The block '' has an error that the variant '' is using a non-existing appearance ''." warning.config.block.state.invalid_vanilla: "Issue found in file - The block '' is using an invalid vanilla block state ''." warning.config.block.state.invalid_auto_state: "Issue found in file - The block '' is using an invalid auto-state ''. Allowed values: []." -warning.config.block.state.auto_state.exhausted: "Issue found in file - Cannot allocate visual block state for block '' as the slots('') in group '' have been exhausted." +warning.config.block.state.auto_state.exhausted: "Issue found in file - The visual state group '' has reached its maximum capacity of '' slots and cannot allocate a state for block ''." warning.config.block.state.unavailable_vanilla: "Issue found in file - The block '' is using an unavailable vanilla block state ''. Please free that state in block-state-mappings." warning.config.block.state.invalid_vanilla_id: "Issue found in file - The block '' is using a vanilla block state '' that exceeds the available slot range '0~'." warning.config.block.state.invalid_id: "Issue found in file - The block state ID range () used by block '' is outside the valid range of 0 to . Please add more server-side blocks in 'config.yml' if the current slots are exhausted." diff --git a/core/src/main/java/net/momirealms/craftengine/core/block/AbstractBlockManager.java b/core/src/main/java/net/momirealms/craftengine/core/block/AbstractBlockManager.java index cd43c04cd..9fcd76918 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/block/AbstractBlockManager.java +++ b/core/src/main/java/net/momirealms/craftengine/core/block/AbstractBlockManager.java @@ -313,6 +313,14 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem this.pendingConfigSections.add(section); } + public IdAllocator internalIdAllocator() { + return internalIdAllocator; + } + + public VisualBlockStateAllocator visualBlockStateAllocator() { + return visualBlockStateAllocator; + } + @Override public void postProcess() { this.internalIdAllocator.processPendingAllocations(); @@ -514,7 +522,7 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem } } - CompletableFutures.allOf(futureVisualStates.values()).whenComplete((v2, t2) -> { + CompletableFutures.allOf(futureVisualStates.values()).whenComplete((v2, t2) -> ResourceConfigUtils.runCatching(path, node, () -> { if (t2 != null) { if (t2 instanceof CompletionException e) { Throwable cause = e.getCause(); @@ -635,7 +643,7 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem // 抛出次要警告 exceptionCollector.throwIfPresent(); - }); + }, () -> GsonHelper.get().toJson(section))); }, () -> GsonHelper.get().toJson(section))); } diff --git a/core/src/main/java/net/momirealms/craftengine/core/font/AbstractFontManager.java b/core/src/main/java/net/momirealms/craftengine/core/font/AbstractFontManager.java index 5c44b4428..f7a3f02b9 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/font/AbstractFontManager.java +++ b/core/src/main/java/net/momirealms/craftengine/core/font/AbstractFontManager.java @@ -32,6 +32,7 @@ import java.util.concurrent.ExecutionException; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; public abstract class AbstractFontManager implements FontManager { private final CraftEngine plugin; @@ -511,10 +512,6 @@ public abstract class AbstractFontManager implements FontManager { }); } - public Map idAllocators() { - return this.idAllocators; - } - @Override public void parseSection(Pack pack, Path path, String node, Key id, Map section) { if (AbstractFontManager.this.images.containsKey(id)) { diff --git a/core/src/main/java/net/momirealms/craftengine/core/pack/allocator/VisualBlockStateAllocator.java b/core/src/main/java/net/momirealms/craftengine/core/pack/allocator/VisualBlockStateAllocator.java index ab7d7cd86..db4576bd0 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/pack/allocator/VisualBlockStateAllocator.java +++ b/core/src/main/java/net/momirealms/craftengine/core/pack/allocator/VisualBlockStateAllocator.java @@ -1,5 +1,7 @@ package net.momirealms.craftengine.core.pack.allocator; +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; @@ -15,6 +17,7 @@ import java.nio.file.Path; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.function.Function; +import java.util.function.Predicate; public class VisualBlockStateAllocator { private final Path cacheFilePath; @@ -32,7 +35,9 @@ public class VisualBlockStateAllocator { } public void reset() { - Arrays.fill(this.pendingAllocationFutures, new ArrayList<>()); + for (int i = 0; i < this.pendingAllocationFutures.length; i++) { + this.pendingAllocationFutures[i] = new ArrayList<>(); + } this.cachedBlockStates.clear(); this.pendingAllocations.clear(); } @@ -53,6 +58,19 @@ public class VisualBlockStateAllocator { return future; } + public List cleanupUnusedIds(Predicate shouldRemove) { + List idsToRemove = new ArrayList<>(); + for (Map.Entry entry : this.cachedBlockStates.entrySet()) { + if (shouldRemove.test(entry.getValue())) { + idsToRemove.add(entry.getKey()); + } + } + for (String id : idsToRemove) { + this.cachedBlockStates.remove(id); + } + return idsToRemove; + } + public void processPendingAllocations() { // 先处理缓存的 for (Map.Entry entry : this.cachedBlockStates.entrySet()) { @@ -64,12 +82,17 @@ public class VisualBlockStateAllocator { if (!candidate.isUsed()) { // 获取当前的安排任务 Pair> pair = this.pendingAllocations.get(entry.getKey()); - // 如果候选满足组,那么直接允许起飞 - if (pair != null && pair.left().test(candidate.blockState())) { - pair.right().complete(candidate.blockState()); + if (pair != null) { + // 如果候选满足组,那么直接允许起飞 + if (pair.left().test(candidate.blockState())) { + pair.right().complete(candidate.blockState()); + } else { + // 不满足候选组要求,那就等着分配新的吧 + } + } else { + // 尽管未被使用,该槽位也应该被占用,以避免被自动分配到 + candidate.setUsed(); } - // 尽管未被使用,该槽位也应该被占用,以避免被自动分配到 - candidate.setUsed(); } // 被使用了就随他去 }