From e64a8d5fed10b8f35bb42701b380cf7f2532afb9 Mon Sep 17 00:00:00 2001 From: XiaoMoMi <972454774@qq.com> Date: Sat, 27 Sep 2025 18:32:02 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E5=88=86=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../advancement/BukkitAdvancementManager.java | 2 +- .../bukkit/item/behavior/AxeItemBehavior.java | 2 +- .../item/behavior/BlockItemBehavior.java | 6 +- .../behavior/CompostableItemBehavior.java | 2 +- .../behavior/DoubleHighBlockItemBehavior.java | 6 +- .../behavior/FlintAndSteelItemBehavior.java | 2 +- .../item/behavior/FurnitureItemBehavior.java | 6 +- .../LiquidCollisionBlockItemBehavior.java | 6 +- .../item/behavior/WallBlockItemBehavior.java | 6 +- .../bukkit/loot/BukkitVanillaLootManager.java | 2 +- common-files/src/main/resources/config.yml | 5 + .../core/block/AbstractBlockManager.java | 28 +- .../furniture/AbstractFurnitureManager.java | 2 +- .../core/font/AbstractFontManager.java | 42 +- .../core/item/AbstractItemManager.java | 559 ++++++++++-------- .../core/item/behavior/EmptyItemBehavior.java | 2 +- .../item/behavior/ItemBehaviorFactory.java | 2 +- .../core/item/behavior/ItemBehaviors.java | 23 +- .../item/recipe/AbstractRecipeManager.java | 4 +- .../core/pack/AbstractPackManager.java | 76 +-- .../craftengine/core/pack/cache/AutoId.java | 200 ------- .../core/pack/cache/IdAllocator.java | 226 +++++++ .../core/plugin/config/Config.java | 26 + .../plugin/config/IdObjectConfigParser.java | 2 +- .../plugin/config/IdSectionConfigParser.java | 2 +- .../config/template/TemplateManagerImpl.java | 4 +- .../plugin/context/GlobalVariableManager.java | 3 +- .../gui/category/ItemBrowserManagerImpl.java | 2 +- .../LocalizedResourceConfigException.java | 2 +- .../plugin/locale/TranslationManagerImpl.java | 6 +- .../core/sound/AbstractSoundManager.java | 4 +- .../core/util/ResourceConfigUtils.java | 30 + 32 files changed, 715 insertions(+), 575 deletions(-) delete mode 100644 core/src/main/java/net/momirealms/craftengine/core/pack/cache/AutoId.java create mode 100644 core/src/main/java/net/momirealms/craftengine/core/pack/cache/IdAllocator.java diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/advancement/BukkitAdvancementManager.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/advancement/BukkitAdvancementManager.java index 1cbff1a44..ee34b3993 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/advancement/BukkitAdvancementManager.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/advancement/BukkitAdvancementManager.java @@ -120,7 +120,7 @@ public class BukkitAdvancementManager extends AbstractAdvancementManager { } @Override - public void parseSection(Pack pack, Path path, Key id, Map section) { + public void parseSection(Pack pack, Path path, String node, Key id, Map section) { if (advancements.containsKey(id)) { throw new LocalizedResourceConfigException("warning.config.advancement.duplicate", path, id); } diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/AxeItemBehavior.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/AxeItemBehavior.java index e928f9233..2cca3f5ae 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/AxeItemBehavior.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/AxeItemBehavior.java @@ -131,7 +131,7 @@ public class AxeItemBehavior extends ItemBehavior { public static class Factory implements ItemBehaviorFactory { @Override - public ItemBehavior create(Pack pack, Path path, Key key, Map arguments) { + public ItemBehavior create(Pack pack, Path path, String node, Key key, Map arguments) { return INSTANCE; } } diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/BlockItemBehavior.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/BlockItemBehavior.java index 45d9c1ef7..a71c46f64 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/BlockItemBehavior.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/BlockItemBehavior.java @@ -234,7 +234,7 @@ public class BlockItemBehavior extends BlockBoundItemBehavior { public static class Factory implements ItemBehaviorFactory { @Override - public ItemBehavior create(Pack pack, Path path, Key key, Map arguments) { + public ItemBehavior create(Pack pack, Path path, String node, Key key, Map arguments) { Object id = arguments.get("block"); if (id == null) { throw new LocalizedResourceConfigException("warning.config.item.behavior.block.missing_block", new IllegalArgumentException("Missing required parameter 'block' for block_item behavior")); @@ -242,9 +242,9 @@ public class BlockItemBehavior extends BlockBoundItemBehavior { if (id instanceof Map map) { if (map.containsKey(key.toString())) { // 防呆 - BukkitBlockManager.instance().blockParser().parseSection(pack, path, key, MiscUtils.castToMap(map.get(key.toString()), false)); + BukkitBlockManager.instance().blockParser().parseSection(pack, path, node, key, MiscUtils.castToMap(map.get(key.toString()), false)); } else { - BukkitBlockManager.instance().blockParser().parseSection(pack, path, key, MiscUtils.castToMap(map, false)); + BukkitBlockManager.instance().blockParser().parseSection(pack, path, node, key, MiscUtils.castToMap(map, false)); } return new BlockItemBehavior(key); } else { diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/CompostableItemBehavior.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/CompostableItemBehavior.java index e4d92d241..50015c355 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/CompostableItemBehavior.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/CompostableItemBehavior.java @@ -79,7 +79,7 @@ public class CompostableItemBehavior extends ItemBehavior { public static class Factory implements ItemBehaviorFactory { @Override - public ItemBehavior create(Pack pack, Path path, Key key, Map arguments) { + public ItemBehavior create(Pack pack, Path path, String node, Key key, Map arguments) { double chance = ResourceConfigUtils.getAsDouble(arguments.getOrDefault("chance", 0.55), "chance"); return new CompostableItemBehavior(chance); } diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/DoubleHighBlockItemBehavior.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/DoubleHighBlockItemBehavior.java index 122bb18b4..403d8eea3 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/DoubleHighBlockItemBehavior.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/DoubleHighBlockItemBehavior.java @@ -40,7 +40,7 @@ public class DoubleHighBlockItemBehavior extends BlockItemBehavior { public static class Factory implements ItemBehaviorFactory { @Override - public ItemBehavior create(Pack pack, Path path, Key key, Map arguments) { + public ItemBehavior create(Pack pack, Path path, String node, Key key, Map arguments) { Object id = arguments.get("block"); if (id == null) { throw new LocalizedResourceConfigException("warning.config.item.behavior.double_high.missing_block", new IllegalArgumentException("Missing required parameter 'block' for double_high_block_item behavior")); @@ -48,9 +48,9 @@ public class DoubleHighBlockItemBehavior extends BlockItemBehavior { if (id instanceof Map map) { if (map.containsKey(key.toString())) { // 防呆 - BukkitBlockManager.instance().blockParser().parseSection(pack, path, key, MiscUtils.castToMap(map.get(key.toString()), false)); + BukkitBlockManager.instance().blockParser().parseSection(pack, path, node, key, MiscUtils.castToMap(map.get(key.toString()), false)); } else { - BukkitBlockManager.instance().blockParser().parseSection(pack, path, key, MiscUtils.castToMap(map, false)); + BukkitBlockManager.instance().blockParser().parseSection(pack, path, node, key, MiscUtils.castToMap(map, false)); } return new DoubleHighBlockItemBehavior(key); } else { diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/FlintAndSteelItemBehavior.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/FlintAndSteelItemBehavior.java index df3e667e0..db9c29878 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/FlintAndSteelItemBehavior.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/FlintAndSteelItemBehavior.java @@ -161,7 +161,7 @@ public class FlintAndSteelItemBehavior extends ItemBehavior { public static class Factory implements ItemBehaviorFactory { @Override - public ItemBehavior create(Pack pack, Path path, Key id, Map arguments) { + public ItemBehavior create(Pack pack, Path path, String node, Key id, Map arguments) { return INSTANCE; } } diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/FurnitureItemBehavior.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/FurnitureItemBehavior.java index c7caee0a0..82a1f8457 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/FurnitureItemBehavior.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/FurnitureItemBehavior.java @@ -177,7 +177,7 @@ public class FurnitureItemBehavior extends ItemBehavior { public static class Factory implements ItemBehaviorFactory { @Override - public ItemBehavior create(Pack pack, Path path, Key key, Map arguments) { + public ItemBehavior create(Pack pack, Path path, String node, Key key, Map arguments) { Object id = arguments.get("furniture"); if (id == null) { throw new LocalizedResourceConfigException("warning.config.item.behavior.furniture.missing_furniture", new IllegalArgumentException("Missing required parameter 'furniture' for furniture_item behavior")); @@ -185,9 +185,9 @@ public class FurnitureItemBehavior extends ItemBehavior { if (id instanceof Map map) { if (map.containsKey(key.toString())) { // 防呆 - BukkitFurnitureManager.instance().parser().parseSection(pack, path, key, MiscUtils.castToMap(map.get(key.toString()), false)); + BukkitFurnitureManager.instance().parser().parseSection(pack, path, node, key, MiscUtils.castToMap(map.get(key.toString()), false)); } else { - BukkitFurnitureManager.instance().parser().parseSection(pack, path, key, MiscUtils.castToMap(map, false)); + BukkitFurnitureManager.instance().parser().parseSection(pack, path, node, key, MiscUtils.castToMap(map, false)); } return new FurnitureItemBehavior(key); } else { diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/LiquidCollisionBlockItemBehavior.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/LiquidCollisionBlockItemBehavior.java index 9e058c549..11a852cac 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/LiquidCollisionBlockItemBehavior.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/LiquidCollisionBlockItemBehavior.java @@ -70,7 +70,7 @@ public class LiquidCollisionBlockItemBehavior extends BlockItemBehavior { public static class Factory implements ItemBehaviorFactory { @Override - public ItemBehavior create(Pack pack, Path path, Key key, Map arguments) { + public ItemBehavior create(Pack pack, Path path, String node, Key key, Map arguments) { Object id = arguments.get("block"); if (id == null) { throw new LocalizedResourceConfigException("warning.config.item.behavior.liquid_collision.missing_block", new IllegalArgumentException("Missing required parameter 'block' for liquid_collision_block_item behavior")); @@ -79,9 +79,9 @@ public class LiquidCollisionBlockItemBehavior extends BlockItemBehavior { if (id instanceof Map map) { if (map.containsKey(key.toString())) { // 防呆 - BukkitBlockManager.instance().blockParser().parseSection(pack, path, key, MiscUtils.castToMap(map.get(key.toString()), false)); + BukkitBlockManager.instance().blockParser().parseSection(pack, path, node, key, MiscUtils.castToMap(map.get(key.toString()), false)); } else { - BukkitBlockManager.instance().blockParser().parseSection(pack, path, key, MiscUtils.castToMap(map, false)); + BukkitBlockManager.instance().blockParser().parseSection(pack, path, node, key, MiscUtils.castToMap(map, false)); } return new LiquidCollisionBlockItemBehavior(key, offset); } else { diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/WallBlockItemBehavior.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/WallBlockItemBehavior.java index bbc48120b..8f8232d4f 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/WallBlockItemBehavior.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/behavior/WallBlockItemBehavior.java @@ -35,7 +35,7 @@ public class WallBlockItemBehavior extends BlockItemBehavior { public static class Factory implements ItemBehaviorFactory { @Override - public ItemBehavior create(Pack pack, Path path, Key key, Map arguments) { + public ItemBehavior create(Pack pack, Path path, String node, Key key, Map arguments) { Object id = arguments.get("block"); if (id == null) { throw new LocalizedResourceConfigException("warning.config.item.behavior.wall_block.missing_block", new IllegalArgumentException("Missing required parameter 'block' for wall_block_item behavior")); @@ -43,9 +43,9 @@ public class WallBlockItemBehavior extends BlockItemBehavior { if (id instanceof Map map) { if (map.containsKey(key.toString())) { // 防呆 - BukkitBlockManager.instance().blockParser().parseSection(pack, path, key, MiscUtils.castToMap(map.get(key.toString()), false)); + BukkitBlockManager.instance().blockParser().parseSection(pack, path, node, key, MiscUtils.castToMap(map.get(key.toString()), false)); } else { - BukkitBlockManager.instance().blockParser().parseSection(pack, path, key, MiscUtils.castToMap(map, false)); + BukkitBlockManager.instance().blockParser().parseSection(pack, path, node, key, MiscUtils.castToMap(map, false)); } return new WallBlockItemBehavior(key); } else { diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/loot/BukkitVanillaLootManager.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/loot/BukkitVanillaLootManager.java index 31ae7e70d..805808e92 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/loot/BukkitVanillaLootManager.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/loot/BukkitVanillaLootManager.java @@ -105,7 +105,7 @@ public class BukkitVanillaLootManager extends AbstractVanillaLootManager impleme } @Override - public void parseSection(Pack pack, Path path, Key id, Map section) { + public void parseSection(Pack pack, Path path, String node, Key id, Map section) { String type = ResourceConfigUtils.requireNonEmptyStringOrThrow(section.get("type"), "warning.config.vanilla_loot.missing_type"); VanillaLoot.Type typeEnum; try { diff --git a/common-files/src/main/resources/config.yml b/common-files/src/main/resources/config.yml index 6a773846c..4f14758e1 100644 --- a/common-files/src/main/resources/config.yml +++ b/common-files/src/main/resources/config.yml @@ -150,6 +150,11 @@ item: click-in-inventory: false # this option won't work for players in creative mode drop: false pick-up: false + # Decided the starting value for automatic custom model data assignment. + custom-model-data-starting-value: + default: 10000 + overrides: + paper: 20000 equipment: # The sacrificed-vanilla-armor argument determines which vanilla armor gets completely removed (loses all its trims) 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 aeeebaea8..9cb662404 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 @@ -20,7 +20,6 @@ import net.momirealms.craftengine.core.plugin.config.*; import net.momirealms.craftengine.core.plugin.context.event.EventFunctions; import net.momirealms.craftengine.core.plugin.locale.LocalizedException; import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException; -import net.momirealms.craftengine.core.plugin.locale.TranslationManager; import net.momirealms.craftengine.core.util.*; import org.incendo.cloud.suggestion.Suggestion; import org.jetbrains.annotations.NotNull; @@ -238,6 +237,7 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem @Override public void parseSection(Pack pack, Path path, Map section) throws LocalizedException { + ExceptionCollector exceptionCollector = new ExceptionCollector<>(); for (Map.Entry entry : section.entrySet()) { String before = entry.getKey(); String after = entry.getValue().toString(); @@ -245,21 +245,25 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem BlockStateWrapper beforeState = createVanillaBlockState(before); BlockStateWrapper afterState = createVanillaBlockState(before); if (beforeState == null) { - TranslationManager.instance().log("warning.config.block_state_mapping.invalid_state", path.toString(), before); - return; + exceptionCollector.add(new LocalizedResourceConfigException("warning.config.block_state_mapping.invalid_state", before)); + continue; } if (afterState == null) { - TranslationManager.instance().log("warning.config.block_state_mapping.invalid_state", path.toString(), after); - return; + exceptionCollector.add(new LocalizedResourceConfigException("warning.config.block_state_mapping.invalid_state", after)); + continue; } int previous = AbstractBlockManager.this.blockStateMappings[beforeState.registryId()]; if (previous != -1 && previous != afterState.registryId()) { - TranslationManager.instance().log("warning.config.block_state_mapping.conflict", path.toString(), beforeState.toString(), afterState.toString(), BlockRegistryMirror.byId(previous).toString()); - return; + exceptionCollector.add(new LocalizedResourceConfigException("warning.config.block_state_mapping.conflict", + beforeState.toString(), + afterState.toString(), + BlockRegistryMirror.byId(previous).toString())); + continue; } AbstractBlockManager.this.blockStateMappings[beforeState.registryId()] = afterState.registryId(); AbstractBlockManager.this.blockStateArranger.computeIfAbsent(getBlockOwnerId(beforeState), k -> new ArrayList<>()).add(afterState); } + exceptionCollector.throwIfPresent(); } } @@ -277,19 +281,19 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem } @Override - public void parseSection(Pack pack, Path path, Key id, Map section) { + public void parseSection(Pack pack, Path path, String node, Key id, Map section) { if (isVanillaBlock(id)) { - parseVanillaBlock(pack, path, id, section); + parseVanillaBlock(id, section); } else { // check duplicated config if (AbstractBlockManager.this.byId.containsKey(id)) { throw new LocalizedResourceConfigException("warning.config.block.duplicate"); } - parseCustomBlock(pack, path, id, section); + parseCustomBlock(id, section); } } - private void parseVanillaBlock(Pack pack, Path path, Key id, Map section) { + private void parseVanillaBlock(Key id, Map section) { Map settings = MiscUtils.castToMap(section.get("settings"), true); if (settings != null) { Object clientBoundTags = settings.get("client-bound-tags"); @@ -300,7 +304,7 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem } } - private void parseCustomBlock(Pack pack, Path path, Key id, Map section) { + private void parseCustomBlock(Key id, Map section) { // 获取方块设置 BlockSettings settings = BlockSettings.fromMap(id, MiscUtils.castToMap(section.get("settings"), true)); // 读取基础外观配置 diff --git a/core/src/main/java/net/momirealms/craftengine/core/entity/furniture/AbstractFurnitureManager.java b/core/src/main/java/net/momirealms/craftengine/core/entity/furniture/AbstractFurnitureManager.java index 3794e85f2..0d5a714d9 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/entity/furniture/AbstractFurnitureManager.java +++ b/core/src/main/java/net/momirealms/craftengine/core/entity/furniture/AbstractFurnitureManager.java @@ -89,7 +89,7 @@ public abstract class AbstractFurnitureManager implements FurnitureManager { @SuppressWarnings("unchecked") @Override - public void parseSection(Pack pack, Path path, Key id, Map section) { + public void parseSection(Pack pack, Path path, String node, Key id, Map section) { if (byId.containsKey(id)) { throw new LocalizedResourceConfigException("warning.config.furniture.duplicate"); } 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 11ef5e872..89721f922 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 @@ -386,18 +386,18 @@ public abstract class AbstractFontManager implements FontManager { } @Override - public void parseSection(Pack pack, Path path, Key id, Map section) { + public void parseSection(Pack pack, Path path, String node, Key id, Map section) { if (emojis.containsKey(id)) { - throw new LocalizedResourceConfigException("warning.config.emoji.duplicate", path, id); + throw new LocalizedResourceConfigException("warning.config.emoji.duplicate"); } String permission = (String) section.get("permission"); Object keywordsRaw = section.get("keywords"); if (keywordsRaw == null) { - throw new LocalizedResourceConfigException("warning.config.emoji.missing_keywords", path, id); + throw new LocalizedResourceConfigException("warning.config.emoji.missing_keywords"); } List keywords = MiscUtils.getAsStringList(keywordsRaw); if (keywords.isEmpty()) { - throw new LocalizedResourceConfigException("warning.config.emoji.missing_keywords", path, id); + throw new LocalizedResourceConfigException("warning.config.emoji.missing_keywords"); } Object rawContent = section.getOrDefault("content", ""); String content; @@ -416,7 +416,7 @@ public abstract class AbstractFontManager implements FontManager { if (bitmapImage.isPresent()) { image = bitmapImage.get().miniMessageAt(0, 0); } else { - throw new LocalizedResourceConfigException("warning.config.emoji.invalid_image", path, id, rawImage); + throw new LocalizedResourceConfigException("warning.config.emoji.invalid_image", rawImage); } } else if (split.length == 4) { Key imageId = new Key(split[0], split[1]); @@ -425,13 +425,13 @@ public abstract class AbstractFontManager implements FontManager { try { image = bitmapImage.get().miniMessageAt(Integer.parseInt(split[2]), Integer.parseInt(split[3])); } catch (ArrayIndexOutOfBoundsException e) { - throw new LocalizedResourceConfigException("warning.config.emoji.invalid_image", path, id, rawImage); + throw new LocalizedResourceConfigException("warning.config.emoji.invalid_image", rawImage); } } else { - throw new LocalizedResourceConfigException("warning.config.emoji.invalid_image", path, id, rawImage); + throw new LocalizedResourceConfigException("warning.config.emoji.invalid_image", rawImage); } } else { - throw new LocalizedResourceConfigException("warning.config.emoji.invalid_image", path, id, rawImage); + throw new LocalizedResourceConfigException("warning.config.emoji.invalid_image", rawImage); } } Emoji emoji = new Emoji(content, permission, image, keywords); @@ -453,24 +453,24 @@ public abstract class AbstractFontManager implements FontManager { } @Override - public void parseSection(Pack pack, Path path, Key id, Map section) { + public void parseSection(Pack pack, Path path, String node, Key id, Map section) { if (images.containsKey(id)) { - throw new LocalizedResourceConfigException("warning.config.image.duplicate", path, id); + throw new LocalizedResourceConfigException("warning.config.image.duplicate"); } Object file = section.get("file"); if (file == null) { - throw new LocalizedResourceConfigException("warning.config.image.missing_file", path, id); + throw new LocalizedResourceConfigException("warning.config.image.missing_file"); } String resourceLocation = CharacterUtils.replaceBackslashWithSlash(file.toString()); if (!ResourceLocation.isValid(resourceLocation)) { - throw new LocalizedResourceConfigException("warning.config.image.invalid_file_chars", path, id, resourceLocation); + throw new LocalizedResourceConfigException("warning.config.image.invalid_file_chars", resourceLocation); } String fontName = section.getOrDefault("font", "minecraft:default").toString(); if (!ResourceLocation.isValid(fontName)) { - throw new LocalizedResourceConfigException("warning.config.image.invalid_font_chars", path, id, fontName); + throw new LocalizedResourceConfigException("warning.config.image.invalid_font_chars", fontName); } Key fontKey = Key.withDefaultNamespace(fontName, id.namespace()); @@ -478,7 +478,7 @@ public abstract class AbstractFontManager implements FontManager { List chars; Object charsObj = ResourceConfigUtils.get(section, "chars", "char"); if (charsObj == null) { - throw new LocalizedResourceConfigException("warning.config.image.missing_char", path, id); + throw new LocalizedResourceConfigException("warning.config.image.missing_char"); } if (charsObj instanceof List list) { chars = MiscUtils.getAsStringList(list).stream().map(it -> { @@ -489,7 +489,7 @@ public abstract class AbstractFontManager implements FontManager { } }).toList(); if (chars.isEmpty()) { - throw new LocalizedResourceConfigException("warning.config.image.missing_char", path, id); + throw new LocalizedResourceConfigException("warning.config.image.missing_char"); } } else { if (charsObj instanceof Integer integer) { @@ -497,7 +497,7 @@ public abstract class AbstractFontManager implements FontManager { } else { String character = charsObj.toString(); if (character.isEmpty()) { - throw new LocalizedResourceConfigException("warning.config.image.missing_char", path, id); + throw new LocalizedResourceConfigException("warning.config.image.missing_char"); } if (character.length() == 1) { chars = List.of(character.toCharArray()); @@ -522,7 +522,7 @@ public abstract class AbstractFontManager implements FontManager { for (int codepoint : codepoints) { if (font.isCodepointInUse(codepoint)) { BitmapImage image = font.bitmapImageByCodepoint(codepoint); - throw new LocalizedResourceConfigException("warning.config.image.codepoint_conflict", path, id, + throw new LocalizedResourceConfigException("warning.config.image.codepoint_conflict", fontKey.toString(), CharacterUtils.encodeCharsToUnicode(Character.toChars(codepoint)), new String(Character.toChars(codepoint)), @@ -530,12 +530,12 @@ public abstract class AbstractFontManager implements FontManager { } } if (codepoints.length == 0) { - throw new LocalizedResourceConfigException("warning.config.image.missing_char", path, id); + throw new LocalizedResourceConfigException("warning.config.image.missing_char"); } codepointGrid[i] = codepoints; if (size == -1) size = codepoints.length; if (size != codepoints.length) { - throw new LocalizedResourceConfigException("warning.config.image.invalid_codepoint_grid", path, id); + throw new LocalizedResourceConfigException("warning.config.image.invalid_codepoint_grid"); } } @@ -558,14 +558,14 @@ public abstract class AbstractFontManager implements FontManager { return; } } else { - throw new LocalizedResourceConfigException("warning.config.image.missing_height", path, id); + throw new LocalizedResourceConfigException("warning.config.image.missing_height"); } } int height = ResourceConfigUtils.getAsInt(heightObj, "height"); int ascent = ResourceConfigUtils.getAsInt(section.getOrDefault("ascent", height - 1), "ascent"); if (height < ascent) { - throw new LocalizedResourceConfigException("warning.config.image.height_ascent_conflict", path, id, String.valueOf(height), String.valueOf(ascent)); + throw new LocalizedResourceConfigException("warning.config.image.height_ascent_conflict", String.valueOf(height), String.valueOf(ascent)); } BitmapImage bitmapImage = new BitmapImage(id, fontKey, height, ascent, resourceLocation, codepointGrid); diff --git a/core/src/main/java/net/momirealms/craftengine/core/item/AbstractItemManager.java b/core/src/main/java/net/momirealms/craftengine/core/item/AbstractItemManager.java index 75b4e4038..b31c26ff3 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/item/AbstractItemManager.java +++ b/core/src/main/java/net/momirealms/craftengine/core/item/AbstractItemManager.java @@ -14,6 +14,7 @@ import net.momirealms.craftengine.core.pack.AbstractPackManager; import net.momirealms.craftengine.core.pack.LoadingSequence; import net.momirealms.craftengine.core.pack.Pack; import net.momirealms.craftengine.core.pack.ResourceLocation; +import net.momirealms.craftengine.core.pack.cache.IdAllocator; import net.momirealms.craftengine.core.pack.model.*; import net.momirealms.craftengine.core.pack.model.generation.AbstractModelGenerator; import net.momirealms.craftengine.core.pack.model.generation.ModelGeneration; @@ -32,8 +33,10 @@ import net.momirealms.craftengine.core.util.*; import org.incendo.cloud.suggestion.Suggestion; import org.incendo.cloud.type.Either; +import java.io.IOException; import java.nio.file.Path; import java.util.*; +import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Stream; @@ -282,7 +285,7 @@ public abstract class AbstractItemManager extends AbstractModelGenerator impl } @Override - public void parseSection(Pack pack, Path path, Key id, Map section) { + public void parseSection(Pack pack, Path path, String node, Key id, Map section) { if (AbstractItemManager.this.equipments.containsKey(id)) { throw new LocalizedResourceConfigException("warning.config.equipment.duplicate"); } @@ -313,6 +316,7 @@ public abstract class AbstractItemManager extends AbstractModelGenerator impl public class ItemParser implements IdSectionConfigParser { public static final String[] CONFIG_SECTION_NAME = new String[] {"items", "item"}; + private final Map idAllocators = new HashMap<>(); private boolean isModernFormatRequired() { return Config.packMaxVersion().isAtOrAbove(MinecraftVersions.V1_21_4); @@ -322,6 +326,18 @@ public abstract class AbstractItemManager extends AbstractModelGenerator impl return Config.packMinVersion().isBelow(MinecraftVersions.V1_21_4); } + private boolean needsCustomModelDataCompatibility() { + return Config.packMinVersion().isBelow(MinecraftVersions.V1_21_2); + } + + private boolean needsItemModelCompatibility() { + return Config.packMaxVersion().isAbove(MinecraftVersions.V1_21_2); + } + + public Map idAllocators() { + return this.idAllocators; + } + @Override public String[] sectionId() { return CONFIG_SECTION_NAME; @@ -333,261 +349,326 @@ public abstract class AbstractItemManager extends AbstractModelGenerator impl } @Override - public void parseSection(Pack pack, Path path, Key id, Map section) { + public void preProcess() { + this.idAllocators.clear(); + } + + @Override + public void postProcess() { + for (Map.Entry entry : this.idAllocators.entrySet()) { + entry.getValue().processPendingAllocations(); + try { + entry.getValue().saveToCache(); + } catch (IOException e) { + AbstractItemManager.this.plugin.logger().warn("Error while saving custom model data allocation for material " + entry.getKey().asString(), e); + } + } + } + + // 创建或获取已有的自动分配器 + private IdAllocator getOrCreateIdAllocator(Key key) { + return this.idAllocators.computeIfAbsent(key, k -> { + IdAllocator newAllocator = new IdAllocator(plugin.dataFolderPath().resolve("cache").resolve("custom-model-data").resolve(k.value() + ".json")); + newAllocator.reset(Config.customModelDataStartingValue(k), 16_777_216); + try { + newAllocator.loadFromCache(); + } catch (IOException e) { + AbstractItemManager.this.plugin.logger().warn("Error while loading custom model data from cache for material " + k.asString(), e); + } + return newAllocator; + }); + } + + @Override + public void parseSection(Pack pack, Path path, String node, Key id, Map section) { if (AbstractItemManager.this.customItemsById.containsKey(id)) { throw new LocalizedResourceConfigException("warning.config.item.duplicate"); } - + // 创建UniqueKey,仅缓存用 UniqueKey uniqueId = UniqueKey.create(id); // 判断是不是原版物品 boolean isVanillaItem = isVanillaItem(id); - Key material = Key.from(isVanillaItem ? id.value() : ResourceConfigUtils.requireNonEmptyStringOrThrow(section.get("material"), "warning.config.item.missing_material").toLowerCase(Locale.ENGLISH)); - Key clientBoundMaterial = section.containsKey("client-bound-material") ? Key.from(section.get("client-bound-material").toString().toLowerCase(Locale.ENGLISH)) : material; - // 如果是原版物品,那么custom-model-data只能是0,即使用户设置了其他值 - int customModelData = isVanillaItem ? 0 : ResourceConfigUtils.getAsInt(section.getOrDefault("custom-model-data", 0), "custom-model-data"); - boolean clientBoundModel = section.containsKey("client-bound-model") ? ResourceConfigUtils.getAsBoolean(section.get("client-bound-model"), "client-bound-model") : Config.globalClientboundModel(); - if (customModelData < 0) { - throw new LocalizedResourceConfigException("warning.config.item.invalid_custom_model_data", String.valueOf(customModelData)); - } - if (customModelData > 16_777_216) { - throw new LocalizedResourceConfigException("warning.config.item.bad_custom_model_data", String.valueOf(customModelData)); - } - // item-model值 - Key itemModelKey = null; + // 读取服务端侧材质 + Key material = isVanillaItem ? id : Key.from(ResourceConfigUtils.requireNonEmptyStringOrThrow(section.get("material"), "warning.config.item.missing_material").toLowerCase(Locale.ROOT)); + // 读取客户端侧材质 + Key clientBoundMaterial = VersionHelper.PREMIUM && section.containsKey("client-bound-material") ? Key.from(section.get("client-bound-material").toString().toLowerCase(Locale.ROOT)) : material; - CustomItem.Builder itemBuilder = createPlatformItemBuilder(uniqueId, material, clientBoundMaterial); - boolean hasItemModelSection = section.containsKey("item-model"); + // custom model data + CompletableFuture customModelDataFuture; - // 如果custom-model-data不为0 - if (customModelData > 0) { - if (clientBoundModel) itemBuilder.clientBoundDataModifier(new CustomModelDataModifier<>(customModelData)); - else itemBuilder.dataModifier(new CustomModelDataModifier<>(customModelData)); - } - // 如果没有item-model选项被配置,同时这个物品又含有 model 区域 - else if (!hasItemModelSection && section.containsKey("model") && VersionHelper.isOrAbove1_21_2()) { - // 那么使用物品id当成item-model的值 - itemModelKey = Key.from(section.getOrDefault("item-model", id.toString()).toString()); - // 但是有个前提,id必须是有效的resource location - if (ResourceLocation.isValid(itemModelKey.toString())) { - if (clientBoundModel) itemBuilder.clientBoundDataModifier(new ItemModelModifier<>(itemModelKey)); - else itemBuilder.dataModifier(new ItemModelModifier<>(itemModelKey)); - } else { - itemModelKey = null; - } - } - - // 如果有item-model - if (hasItemModelSection && VersionHelper.isOrAbove1_21_2()) { - itemModelKey = Key.from(section.get("item-model").toString()); - if (clientBoundModel) itemBuilder.clientBoundDataModifier(new ItemModelModifier<>(itemModelKey)); - else itemBuilder.dataModifier(new ItemModelModifier<>(itemModelKey)); - } - - // 对于不重要的配置,可以仅警告,不返回 - ExceptionCollector collector = new ExceptionCollector<>(); - - // 应用物品数据 - try { - applyDataModifiers(MiscUtils.castToMap(section.get("data"), true), itemBuilder::dataModifier); - } catch (LocalizedResourceConfigException e) { - collector.add(e); - } - // 应用客户端侧数据 - try { - if (VersionHelper.PREMIUM) { - applyDataModifiers(MiscUtils.castToMap(section.get("client-bound-data"), true), itemBuilder::clientBoundDataModifier); - } - } catch (LocalizedResourceConfigException e) { - collector.add(e); - } - - // 如果不是原版物品,那么加入ce的标识符 - if (!isVanillaItem) - itemBuilder.dataModifier(new IdModifier<>(id)); - - // 事件 - Map>> eventTriggerListMap; - try { - eventTriggerListMap = EventFunctions.parseEvents(ResourceConfigUtils.get(section, "events", "event")); - } catch (LocalizedResourceConfigException e) { - collector.add(e); - eventTriggerListMap = Map.of(); - } - - // 设置 - ItemSettings settings; - try { - settings = Optional.ofNullable(ResourceConfigUtils.get(section, "settings")) - .map(map -> ItemSettings.fromMap(MiscUtils.castToMap(map, true))) - .map(it -> isVanillaItem ? it.canPlaceRelatedVanillaBlock(true) : it) - .orElse(ItemSettings.of().canPlaceRelatedVanillaBlock(isVanillaItem)); - } catch (LocalizedResourceConfigException e) { - collector.add(e); - settings = ItemSettings.of().canPlaceRelatedVanillaBlock(isVanillaItem); - } - - // 行为 - List behaviors; - try { - behaviors = ItemBehaviors.fromObj(pack, path, id, ResourceConfigUtils.get(section, "behavior", "behaviors")); - } catch (LocalizedResourceConfigException e) { - collector.add(e); - behaviors = Collections.emptyList(); - } - - // 如果有物品更新器 - if (section.containsKey("updater")) { - Map updater = ResourceConfigUtils.getAsMap(section.get("updater"), "updater"); - List versions = new ArrayList<>(2); - for (Map.Entry entry : updater.entrySet()) { - try { - int version = Integer.parseInt(entry.getKey()); - versions.add(new ItemUpdateConfig.Version( - version, - ResourceConfigUtils.parseConfigAsList(entry.getValue(), map -> ItemUpdaters.fromMap(id, map)).toArray(new ItemUpdater[0]) - )); - } catch (NumberFormatException ignored) { - } - } - ItemUpdateConfig config = new ItemUpdateConfig(versions); - itemBuilder.updater(config); - itemBuilder.dataModifier(new ItemVersionModifier<>(config.maxVersion())); - } - - // 构建自定义物品 - CustomItem customItem = itemBuilder - .isVanillaItem(isVanillaItem) - .behaviors(behaviors) - .settings(settings) - .events(eventTriggerListMap) - .build(); - - // 添加到缓存 - addCustomItem(customItem); - - // 如果有类别,则添加 - if (section.containsKey("category")) { - AbstractItemManager.this.plugin.itemBrowserManager().addExternalCategoryMember(id, MiscUtils.getAsStringList(section.get("category")).stream().map(Key::of).toList()); - } - - // 模型配置区域,如果这里被配置了,那么用户必须要配置custom-model-data或item-model - Map modelSection = MiscUtils.castToMap(section.get("model"), true); - Map legacyModelSection = MiscUtils.castToMap(section.get("legacy-model"), true); - if (modelSection == null && legacyModelSection == null) { - collector.throwIfPresent(); - return; - } - - boolean needsModelSection = isModernFormatRequired() || (needsLegacyCompatibility() && legacyModelSection == null); - // 只对自定义物品有这个限制 if (!isVanillaItem) { - // 既没有模型值也没有item-model - if (customModelData == 0 && itemModelKey == null) { - collector.addAndThrow(new LocalizedResourceConfigException("warning.config.item.missing_model_id")); + // 如果用户指定了,说明要手动分配,不管他是什么版本,都强制设置模型值 + if (section.containsKey("custom-model-data")) { + int customModelData = ResourceConfigUtils.getAsInt(section.getOrDefault("custom-model-data", 0), "custom-model-data"); + if (customModelData < 0) { + throw new LocalizedResourceConfigException("warning.config.item.invalid_custom_model_data", String.valueOf(customModelData)); + } + if (customModelData > 16_777_216) { + throw new LocalizedResourceConfigException("warning.config.item.bad_custom_model_data", String.valueOf(customModelData)); + } + customModelDataFuture = getOrCreateIdAllocator(clientBoundMaterial).assignFixedId(id.asString(), customModelData); } - } - - // 新版格式 - ItemModel modernModel = null; - // 旧版格式 - TreeSet legacyOverridesModels = null; - // 如果需要支持新版item model 或者用户需要旧版本兼容,但是没配置legacy-model - if (needsModelSection) { - // 1.21.4+必须要配置model区域,如果不需要高版本兼容,则可以只写legacy-model - if (modelSection == null) { - collector.addAndThrow(new LocalizedResourceConfigException("warning.config.item.missing_model")); - return; - } - try { - modernModel = ItemModels.fromMap(modelSection); - for (ModelGeneration generation : modernModel.modelsToGenerate()) { - prepareModelGeneration(generation); + // 用户没指定custom-model-data,则看当前资源包版本兼容需求 + else { + // 如果最低版本要1.21.1以下支持 + if (needsCustomModelDataCompatibility()) { + customModelDataFuture = getOrCreateIdAllocator(clientBoundMaterial).requestAutoId(id.asString()); } - } catch (LocalizedResourceConfigException e) { - collector.addAndThrow(e); - } - } - // 如果需要旧版本兼容 - if (needsLegacyCompatibility()) { - if (legacyModelSection != null) { - try { - LegacyItemModel legacyItemModel = LegacyItemModel.fromMap(legacyModelSection, customModelData); - for (ModelGeneration generation : legacyItemModel.modelsToGenerate()) { - prepareModelGeneration(generation); - } - legacyOverridesModels = new TreeSet<>(legacyItemModel.overrides()); - } catch (LocalizedResourceConfigException e) { - collector.addAndThrow(e); - } - } else { - legacyOverridesModels = new TreeSet<>(); - processModelRecursively(modernModel, new LinkedHashMap<>(), legacyOverridesModels, clientBoundMaterial, customModelData); - if (legacyOverridesModels.isEmpty()) { - collector.add(new LocalizedResourceConfigException("warning.config.item.legacy_model.cannot_convert", path.toString(), id.asString())); - } - } - } - - // 自定义物品的model处理 - if (!isVanillaItem) { - // 这个item-model是否存在,且是原版item-model - boolean isVanillaItemModel = itemModelKey != null && AbstractPackManager.PRESET_ITEMS.containsKey(itemModelKey); - // 使用了自定义模型值 - if (customModelData != 0) { - // 如果用户主动设置了item-model且为原版物品,则使用item-model为基础模型,否则使用其视觉材质对应的item-model - Key finalBaseModel = isVanillaItemModel ? itemModelKey : clientBoundMaterial; - // 检查cmd冲突 - Map conflict = AbstractItemManager.this.cmdConflictChecker.computeIfAbsent(finalBaseModel, k -> new HashMap<>()); - if (conflict.containsKey(customModelData)) { - collector.addAndThrow(new LocalizedResourceConfigException("warning.config.item.custom_model_data_conflict", String.valueOf(customModelData), conflict.get(customModelData).toString())); - } - conflict.put(customModelData, id); - // 添加新版item model - if (isModernFormatRequired() && modernModel != null) { - TreeMap map = AbstractItemManager.this.modernOverrides.computeIfAbsent(finalBaseModel, k -> new TreeMap<>()); - map.put(customModelData, new ModernItemModel( - modernModel, - ResourceConfigUtils.getAsBoolean(section.getOrDefault("oversized-in-gui", true), "oversized-in-gui"), - ResourceConfigUtils.getAsBoolean(section.getOrDefault("hand-animation-on-swap", true), "hand-animation-on-swap") - )); - } - // 添加旧版 overrides - if (needsLegacyCompatibility() && legacyOverridesModels != null && !legacyOverridesModels.isEmpty()) { - TreeSet lom = AbstractItemManager.this.legacyOverrides.computeIfAbsent(finalBaseModel, k -> new TreeSet<>()); - lom.addAll(legacyOverridesModels); - } - } else if (isVanillaItemModel) { - collector.addAndThrow(new LocalizedResourceConfigException("warning.config.item.item_model.conflict", itemModelKey.asString())); - } - - // 使用了item-model组件,且不是原版物品的 - if (itemModelKey != null && !isVanillaItemModel) { - if (isModernFormatRequired() && modernModel != null) { - AbstractItemManager.this.modernItemModels1_21_4.put(itemModelKey, new ModernItemModel( - modernModel, - ResourceConfigUtils.getAsBoolean(section.getOrDefault("oversized-in-gui", true), "oversized-in-gui"), - ResourceConfigUtils.getAsBoolean(section.getOrDefault("hand-animation-on-swap", true), "hand-animation-on-swap") - )); - } - if (Config.packMaxVersion().isAtOrAbove(MinecraftVersions.V1_21_2) && needsLegacyCompatibility() && legacyOverridesModels != null && !legacyOverridesModels.isEmpty()) { - TreeSet lom = AbstractItemManager.this.modernItemModels1_21_2.computeIfAbsent(itemModelKey, k -> new TreeSet<>()); - lom.addAll(legacyOverridesModels); + // 否则不主动分配模型值 + else { + customModelDataFuture = CompletableFuture.completedFuture(0); } } } else { - // 原版物品的item model覆写 - if (isModernFormatRequired()) { - AbstractItemManager.this.modernItemModels1_21_4.put(id, new ModernItemModel( - modernModel, - ResourceConfigUtils.getAsBoolean(section.getOrDefault("oversized-in-gui", true), "oversized-in-gui"), - ResourceConfigUtils.getAsBoolean(section.getOrDefault("hand-animation-on-swap", true), "hand-animation-on-swap") - )); - } + // 原版物品不应该有这个 + customModelDataFuture = CompletableFuture.completedFuture(0); } - // 抛出异常 - collector.throwIfPresent(); + // 是否使用客户端侧模型 + boolean clientBoundModel = VersionHelper.PREMIUM && (section.containsKey("client-bound-model") ? ResourceConfigUtils.getAsBoolean(section.get("client-bound-model"), "client-bound-model") : Config.globalClientboundModel()); + + // 当模型值完成分配的时候 + customModelDataFuture.thenAccept(customModelData -> ResourceConfigUtils.runCatching(path, node, () -> { + + // item model + Key itemModel = null; + + // 如果这个版本可以使用 item model + if (!isVanillaItem && needsItemModelCompatibility()) { + // 如果用户主动设定了item model,那么肯定要设置 + if (section.containsKey("item-model")) { + itemModel = Key.from(section.get("item-model").toString()); + } + // 用户没设置item model也没设置custom model data,那么为他生成一个基于物品id的item model + else if (customModelData == 0) { + itemModel = id; + } + // 用户没设置item model但是有custom model data,那么就使用custom model data + } + + CustomItem.Builder itemBuilder = createPlatformItemBuilder(uniqueId, material, clientBoundMaterial); + if (customModelData > 0) { + if (clientBoundModel) itemBuilder.clientBoundDataModifier(new CustomModelDataModifier<>(customModelData)); + else itemBuilder.dataModifier(new CustomModelDataModifier<>(customModelData)); + } + if (itemModel != null) { + if (clientBoundModel) itemBuilder.clientBoundDataModifier(new ItemModelModifier<>(itemModel)); + else itemBuilder.dataModifier(new ItemModelModifier<>(itemModel)); + } + + // 对于不重要的配置,可以仅警告,不返回 + ExceptionCollector collector = new ExceptionCollector<>(); + + // 应用物品数据 + try { + applyDataModifiers(MiscUtils.castToMap(section.get("data"), true), itemBuilder::dataModifier); + } catch (LocalizedResourceConfigException e) { + collector.add(e); + } + + // 应用客户端侧数据 + try { + if (VersionHelper.PREMIUM) { + applyDataModifiers(MiscUtils.castToMap(section.get("client-bound-data"), true), itemBuilder::clientBoundDataModifier); + } + } catch (LocalizedResourceConfigException e) { + collector.add(e); + } + + // 如果不是原版物品,那么加入ce的标识符 + if (!isVanillaItem) + itemBuilder.dataModifier(new IdModifier<>(id)); + + // 事件 + Map>> eventTriggerListMap; + try { + eventTriggerListMap = EventFunctions.parseEvents(ResourceConfigUtils.get(section, "events", "event")); + } catch (LocalizedResourceConfigException e) { + collector.add(e); + eventTriggerListMap = Map.of(); + } + + // 设置 + ItemSettings settings; + try { + settings = Optional.ofNullable(ResourceConfigUtils.get(section, "settings")) + .map(map -> ItemSettings.fromMap(MiscUtils.castToMap(map, true))) + .map(it -> isVanillaItem ? it.canPlaceRelatedVanillaBlock(true) : it) + .orElse(ItemSettings.of().canPlaceRelatedVanillaBlock(isVanillaItem)); + } catch (LocalizedResourceConfigException e) { + collector.add(e); + settings = ItemSettings.of().canPlaceRelatedVanillaBlock(isVanillaItem); + } + + // 行为 + List behaviors; + try { + behaviors = ResourceConfigUtils.parseConfigAsList(ResourceConfigUtils.get(section, "behavior", "behaviors"), map -> ItemBehaviors.fromMap(pack, path, node, id, map)); + } catch (LocalizedResourceConfigException e) { + collector.add(e); + behaviors = Collections.emptyList(); + } + + // 如果有物品更新器 + if (section.containsKey("updater")) { + Map updater = ResourceConfigUtils.getAsMap(section.get("updater"), "updater"); + List versions = new ArrayList<>(2); + for (Map.Entry entry : updater.entrySet()) { + try { + int version = Integer.parseInt(entry.getKey()); + versions.add(new ItemUpdateConfig.Version( + version, + ResourceConfigUtils.parseConfigAsList(entry.getValue(), map -> ItemUpdaters.fromMap(id, map)).toArray(new ItemUpdater[0]) + )); + } catch (NumberFormatException ignored) { + } + } + ItemUpdateConfig config = new ItemUpdateConfig(versions); + itemBuilder.updater(config); + itemBuilder.dataModifier(new ItemVersionModifier<>(config.maxVersion())); + } + + // 构建自定义物品 + CustomItem customItem = itemBuilder + .isVanillaItem(isVanillaItem) + .behaviors(behaviors) + .settings(settings) + .events(eventTriggerListMap) + .build(); + + // 添加到缓存 + addCustomItem(customItem); + + // 如果有类别,则添加 + if (section.containsKey("category")) { + AbstractItemManager.this.plugin.itemBrowserManager().addExternalCategoryMember(id, MiscUtils.getAsStringList(section.get("category")).stream().map(Key::of).toList()); + } + + /* + * ======================== + * + * 模型配置分界线 + * + * ======================== + */ + + // 模型配置区域,如果这里被配置了,那么用户必须要配置custom-model-data或item-model + Map modelSection = MiscUtils.castToMap(section.get("model"), true); + Map legacyModelSection = MiscUtils.castToMap(section.get("legacy-model"), true); + if (modelSection == null && legacyModelSection == null) { + collector.throwIfPresent(); + return; + } + + boolean needsModelSection = isModernFormatRequired() || (needsLegacyCompatibility() && legacyModelSection == null); + // 只对自定义物品有这个限制,既没有模型值也没有item-model + if (!isVanillaItem && customModelData == 0 && itemModel == null) { + collector.addAndThrow(new LocalizedResourceConfigException("warning.config.item.missing_model_id")); + } + + // 新版格式 + ItemModel modernModel = null; + // 旧版格式 + TreeSet legacyOverridesModels = null; + // 如果需要支持新版item model 或者用户需要旧版本兼容,但是没配置legacy-model + if (needsModelSection) { + // 1.21.4+必须要配置model区域,如果不需要高版本兼容,则可以只写legacy-model + if (modelSection == null) { + collector.addAndThrow(new LocalizedResourceConfigException("warning.config.item.missing_model")); + return; + } + try { + modernModel = ItemModels.fromMap(modelSection); + for (ModelGeneration generation : modernModel.modelsToGenerate()) { + prepareModelGeneration(generation); + } + } catch (LocalizedResourceConfigException e) { + collector.addAndThrow(e); + } + } + // 如果需要旧版本兼容 + if (needsLegacyCompatibility()) { + if (legacyModelSection != null) { + try { + LegacyItemModel legacyItemModel = LegacyItemModel.fromMap(legacyModelSection, customModelData); + for (ModelGeneration generation : legacyItemModel.modelsToGenerate()) { + prepareModelGeneration(generation); + } + legacyOverridesModels = new TreeSet<>(legacyItemModel.overrides()); + } catch (LocalizedResourceConfigException e) { + collector.addAndThrow(e); + } + } else { + legacyOverridesModels = new TreeSet<>(); + processModelRecursively(modernModel, new LinkedHashMap<>(), legacyOverridesModels, clientBoundMaterial, customModelData); + if (legacyOverridesModels.isEmpty()) { + collector.add(new LocalizedResourceConfigException("warning.config.item.legacy_model.cannot_convert", path.toString(), id.asString())); + } + } + } + + // 自定义物品的model处理 + if (!isVanillaItem) { + // 这个item-model是否存在,且是原版item-model + boolean isVanillaItemModel = itemModel != null && AbstractPackManager.PRESET_ITEMS.containsKey(itemModel); + // 使用了自定义模型值 + if (customModelData != 0) { + // 如果用户主动设置了item-model且为原版物品,则使用item-model为基础模型,否则使用其视觉材质对应的item-model + Key finalBaseModel = isVanillaItemModel ? itemModel : clientBoundMaterial; + // 检查cmd冲突 + Map conflict = AbstractItemManager.this.cmdConflictChecker.computeIfAbsent(finalBaseModel, k -> new HashMap<>()); + if (conflict.containsKey(customModelData)) { + collector.addAndThrow(new LocalizedResourceConfigException("warning.config.item.custom_model_data_conflict", String.valueOf(customModelData), conflict.get(customModelData).toString())); + } + conflict.put(customModelData, id); + // 添加新版item model + if (isModernFormatRequired() && modernModel != null) { + TreeMap map = AbstractItemManager.this.modernOverrides.computeIfAbsent(finalBaseModel, k -> new TreeMap<>()); + map.put(customModelData, new ModernItemModel( + modernModel, + ResourceConfigUtils.getAsBoolean(section.getOrDefault("oversized-in-gui", true), "oversized-in-gui"), + ResourceConfigUtils.getAsBoolean(section.getOrDefault("hand-animation-on-swap", true), "hand-animation-on-swap") + )); + } + // 添加旧版 overrides + if (needsLegacyCompatibility() && legacyOverridesModels != null && !legacyOverridesModels.isEmpty()) { + TreeSet lom = AbstractItemManager.this.legacyOverrides.computeIfAbsent(finalBaseModel, k -> new TreeSet<>()); + lom.addAll(legacyOverridesModels); + } + } else if (isVanillaItemModel) { + collector.addAndThrow(new LocalizedResourceConfigException("warning.config.item.item_model.conflict", itemModel.asString())); + } + + // 使用了item-model组件,且不是原版物品的 + if (itemModel != null && !isVanillaItemModel) { + if (isModernFormatRequired() && modernModel != null) { + AbstractItemManager.this.modernItemModels1_21_4.put(itemModel, new ModernItemModel( + modernModel, + ResourceConfigUtils.getAsBoolean(section.getOrDefault("oversized-in-gui", true), "oversized-in-gui"), + ResourceConfigUtils.getAsBoolean(section.getOrDefault("hand-animation-on-swap", true), "hand-animation-on-swap") + )); + } + if (Config.packMaxVersion().isAtOrAbove(MinecraftVersions.V1_21_2) && needsLegacyCompatibility() && legacyOverridesModels != null && !legacyOverridesModels.isEmpty()) { + TreeSet lom = AbstractItemManager.this.modernItemModels1_21_2.computeIfAbsent(itemModel, k -> new TreeSet<>()); + lom.addAll(legacyOverridesModels); + } + } + } else { + // 原版物品的item model覆写 + if (isModernFormatRequired()) { + AbstractItemManager.this.modernItemModels1_21_4.put(id, new ModernItemModel( + modernModel, + ResourceConfigUtils.getAsBoolean(section.getOrDefault("oversized-in-gui", true), "oversized-in-gui"), + ResourceConfigUtils.getAsBoolean(section.getOrDefault("hand-animation-on-swap", true), "hand-animation-on-swap") + )); + } + } + + // 抛出异常 + collector.throwIfPresent(); + + }, () -> GsonHelper.get().toJson(section))); } } diff --git a/core/src/main/java/net/momirealms/craftengine/core/item/behavior/EmptyItemBehavior.java b/core/src/main/java/net/momirealms/craftengine/core/item/behavior/EmptyItemBehavior.java index 68c0c1e49..6546c56c3 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/item/behavior/EmptyItemBehavior.java +++ b/core/src/main/java/net/momirealms/craftengine/core/item/behavior/EmptyItemBehavior.java @@ -13,7 +13,7 @@ public class EmptyItemBehavior extends ItemBehavior { public static class Factory implements ItemBehaviorFactory { @Override - public ItemBehavior create(Pack pack, Path path, Key id, Map arguments) { + public ItemBehavior create(Pack pack, Path path, String node, Key id, Map arguments) { return INSTANCE; } } diff --git a/core/src/main/java/net/momirealms/craftengine/core/item/behavior/ItemBehaviorFactory.java b/core/src/main/java/net/momirealms/craftengine/core/item/behavior/ItemBehaviorFactory.java index 4b5ebe888..0f90c6223 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/item/behavior/ItemBehaviorFactory.java +++ b/core/src/main/java/net/momirealms/craftengine/core/item/behavior/ItemBehaviorFactory.java @@ -8,5 +8,5 @@ import java.util.Map; public interface ItemBehaviorFactory { - ItemBehavior create(Pack pack, Path path, Key id, Map arguments); + ItemBehavior create(Pack pack, Path path, String node, Key id, Map arguments); } diff --git a/core/src/main/java/net/momirealms/craftengine/core/item/behavior/ItemBehaviors.java b/core/src/main/java/net/momirealms/craftengine/core/item/behavior/ItemBehaviors.java index 6ab3f1d38..21399de3d 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/item/behavior/ItemBehaviors.java +++ b/core/src/main/java/net/momirealms/craftengine/core/item/behavior/ItemBehaviors.java @@ -23,7 +23,7 @@ public class ItemBehaviors { .register(ResourceKey.create(Registries.ITEM_BEHAVIOR_FACTORY.location(), key), factory); } - public static ItemBehavior fromMap(Pack pack, Path path, Key id, Map map) { + public static ItemBehavior fromMap(Pack pack, Path path, String node, Key id, Map map) { if (map == null || map.isEmpty()) return EmptyItemBehavior.INSTANCE; String type = ResourceConfigUtils.requireNonEmptyStringOrThrow(map.get("type"), "warning.config.item.behavior.missing_type"); Key key = Key.withDefaultNamespace(type, Key.DEFAULT_NAMESPACE); @@ -31,25 +31,6 @@ public class ItemBehaviors { if (factory == null) { throw new LocalizedResourceConfigException("warning.config.item.behavior.invalid_type", type); } - return factory.create(pack, path, id, map); - } - - public static List fromList(Pack pack, Path path, Key id, List> list) { - List behaviors = new ArrayList<>(list.size()); - for (Map map : list) { - behaviors.add(fromMap(pack, path, id, map)); - } - return behaviors; - } - - @SuppressWarnings("unchecked") - public static List fromObj(Pack pack, Path path, Key id, Object behaviorObj) { - if (behaviorObj instanceof Map) { - return List.of(fromMap(pack, path, id, MiscUtils.castToMap(behaviorObj, false))); - } else if (behaviorObj instanceof List) { - return fromList(pack, path, id, (List>) behaviorObj); - } else { - return List.of(); - } + return factory.create(pack, path, node, id, map); } } \ No newline at end of file diff --git a/core/src/main/java/net/momirealms/craftengine/core/item/recipe/AbstractRecipeManager.java b/core/src/main/java/net/momirealms/craftengine/core/item/recipe/AbstractRecipeManager.java index c9db7e549..6a9fc4273 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/item/recipe/AbstractRecipeManager.java +++ b/core/src/main/java/net/momirealms/craftengine/core/item/recipe/AbstractRecipeManager.java @@ -135,10 +135,10 @@ public abstract class AbstractRecipeManager implements RecipeManager { } @Override - public void parseSection(Pack pack, Path path, Key id, Map section) { + public void parseSection(Pack pack, Path path, String node, Key id, Map section) { if (!Config.enableRecipeSystem()) return; if (AbstractRecipeManager.this.byId.containsKey(id)) { - throw new LocalizedResourceConfigException("warning.config.recipe.duplicate", path, id); + throw new LocalizedResourceConfigException("warning.config.recipe.duplicate"); } Recipe recipe = RecipeSerializers.fromMap(id, section); try { diff --git a/core/src/main/java/net/momirealms/craftengine/core/pack/AbstractPackManager.java b/core/src/main/java/net/momirealms/craftengine/core/pack/AbstractPackManager.java index eab304ce6..268e9bdc9 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/pack/AbstractPackManager.java +++ b/core/src/main/java/net/momirealms/craftengine/core/pack/AbstractPackManager.java @@ -608,7 +608,6 @@ public abstract class AbstractPackManager implements PackManager { return cachedConfigs; } - // todo 本地化日志 private void loadResourceConfigs(Predicate predicate) { long o1 = System.nanoTime(); TreeMap> cachedConfigs = this.updateCachedConfigFiles(); @@ -622,13 +621,12 @@ public abstract class AbstractPackManager implements PackManager { switch (parser) { case SectionConfigParser configParser -> { for (CachedConfigSection cached : entry.getValue()) { - try { - configParser.parseSection(cached.pack(), cached.filePath(), cached.config()); - } catch (LocalizedException e) { - printWarningRecursively(e, cached.filePath(), cached.prefix()); - } catch (Exception e) { - this.plugin.logger().warn("Unexpected error loading file " + cached.filePath() + " - '" + parser.sectionId()[0] + "'. Please find the cause according to the stacktrace or seek developer help. Additional info: " + GsonHelper.get().toJson(cached.config()), e); - } + ResourceConfigUtils.runCatching( + cached.filePath(), + cached.prefix(), + () -> configParser.parseSection(cached.pack(), cached.filePath(), cached.config()), + () -> GsonHelper.get().toJson(cached.config()) + ); } } case IdObjectConfigParser configParser -> { @@ -636,13 +634,13 @@ public abstract class AbstractPackManager implements PackManager { for (Map.Entry configEntry : cached.config().entrySet()) { String key = configEntry.getKey(); Key id = Key.withDefaultNamespace(key, cached.pack().namespace()); - try { - configParser.parseObject(cached.pack(), cached.filePath(), id, configEntry.getValue()); - } catch (LocalizedException e) { - printWarningRecursively(e, cached.filePath(), cached.prefix() + "." + key); - } catch (Exception e) { - this.plugin.logger().warn("Unexpected error loading file " + cached.filePath() + " - '" + parser.sectionId()[0] + "." + key + "'. Please find the cause according to the stacktrace or seek developer help. Additional info: " + GsonHelper.get().toJson(configEntry.getValue()), e); - } + String node = cached.prefix() + "." + key; + ResourceConfigUtils.runCatching( + cached.filePath(), + node, + () -> configParser.parseObject(cached.pack(), cached.filePath(), node, id, configEntry.getValue()), + () -> GsonHelper.get().toJson(configEntry.getValue()) + ); } } } @@ -651,49 +649,37 @@ public abstract class AbstractPackManager implements PackManager { for (Map.Entry configEntry : cached.config().entrySet()) { String key = configEntry.getKey(); Key id = Key.withDefaultNamespace(key, cached.pack().namespace()); - try { - if (configEntry.getValue() instanceof Map configSection0) { - Map config = castToMap(configSection0, false); - if ((boolean) config.getOrDefault("debug", false)) { - this.plugin.logger().info(GsonHelper.get().toJson(this.plugin.templateManager().applyTemplates(id, config))); - } - if ((boolean) config.getOrDefault("enable", true)) { - configParser.parseSection(cached.pack(), cached.filePath(), id, MiscUtils.castToMap(this.plugin.templateManager().applyTemplates(id, config), false)); - } - } else { - TranslationManager.instance().log("warning.config.structure.not_section", cached.filePath().toString(), cached.prefix() + "." + key, configEntry.getValue().getClass().getSimpleName()); - } - } catch (LocalizedException e) { - printWarningRecursively(e, cached.filePath(), cached.prefix() + "." + key); - } catch (Exception e) { - this.plugin.logger().warn("Unexpected error loading file " + cached.filePath() + " - '" + parser.sectionId()[0] + "." + key + "'. Please find the cause according to the stacktrace or seek developer help. Additional info: " + GsonHelper.get().toJson(configEntry.getValue()), e); + if (!(configEntry.getValue() instanceof Map section)) { + TranslationManager.instance().log("warning.config.structure.not_section", + cached.filePath().toString(), cached.prefix() + "." + key, configEntry.getValue().getClass().getSimpleName()); + continue; } + Map config = castToMap(section, false); + if ((boolean) config.getOrDefault("debug", false)) { + this.plugin.logger().info(GsonHelper.get().toJson(this.plugin.templateManager().applyTemplates(id, config))); + } + if (!(boolean) config.getOrDefault("enable", true)) { + continue; + } + String node = cached.prefix() + "." + key; + ResourceConfigUtils.runCatching( + cached.filePath(), + node, + () -> configParser.parseSection(cached.pack(), cached.filePath(), node, id, MiscUtils.castToMap(this.plugin.templateManager().applyTemplates(id, config), false)), + () -> GsonHelper.get().toJson(section) + ); } } } default -> { } } - parser.postProcess(); long t2 = System.nanoTime(); this.plugin.logger().info("Loaded " + parser.sectionId()[0] + " in " + String.format("%.2f", ((t2 - t1) / 1_000_000.0)) + " ms"); } } - private void printWarningRecursively(LocalizedException e, Path path, String prefix) { - for (Throwable t : e.getSuppressed()) { - if (t instanceof LocalizedException suppressed) { - printWarningRecursively(suppressed, path, prefix); - } - } - if (e instanceof LocalizedResourceConfigException exception) { - exception.setPath(path); - exception.setId(prefix); - } - TranslationManager.instance().log(e.node(), e.arguments()); - } - private void processConfigEntry(Map.Entry entry, Path path, Pack pack, BiConsumer callback) { if (entry.getValue() instanceof Map typeSections0) { String key = entry.getKey(); diff --git a/core/src/main/java/net/momirealms/craftengine/core/pack/cache/AutoId.java b/core/src/main/java/net/momirealms/craftengine/core/pack/cache/AutoId.java deleted file mode 100644 index 15e4cf7fc..000000000 --- a/core/src/main/java/net/momirealms/craftengine/core/pack/cache/AutoId.java +++ /dev/null @@ -1,200 +0,0 @@ -package net.momirealms.craftengine.core.pack.cache; - -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; -import net.momirealms.craftengine.core.util.FileUtils; -import net.momirealms.craftengine.core.util.GsonHelper; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.function.Predicate; - -public class AutoId { - private final Path cachePath; - private final BiMap forcedIds = HashBiMap.create(128); - - private final Map cachedIds = new HashMap<>(); - private final BitSet occupiedIds = new BitSet(); - private final Map> autoIds = new HashMap<>(); - private int currentAutoId; - private int minId; - private int maxId; - - public AutoId(Path cachePath) { - this.cachePath = cachePath; - } - - public void reset(int startIndex, int endIndex) { - this.minId = startIndex; - this.currentAutoId = startIndex; - this.maxId = endIndex; - this.occupiedIds.clear(); - this.forcedIds.clear(); - this.autoIds.clear(); - this.cachedIds.clear(); - } - - public void arrangeForTheRest() { - // 然后处理自动分配的ID - for (Map.Entry> entry : this.autoIds.entrySet()) { - String name = entry.getKey(); - CompletableFuture future = entry.getValue(); - - // 不应该触发 - if (future.isDone()) { - continue; - } - - // 尝试使用缓存的ID,并且其有效 - Integer cachedId = this.cachedIds.get(name); - if (cachedId != null && !this.occupiedIds.get(cachedId) && cachedId >= this.minId && cachedId <= this.maxId) { - this.occupiedIds.set(cachedId); - future.complete(cachedId); - continue; - } - - // 寻找下一个可用的自动ID - int autoId = findNextAvailableAutoId(); - if (autoId == -1) { - // 没有可用的ID - future.completeExceptionally(new AutoIdExhaustedException(name, this.minId, this.maxId)); - continue; - } - - // 分配找到的ID - this.occupiedIds.set(autoId); - future.complete(autoId); - this.cachedIds.put(name, autoId); - } - - // 清空futureIds,因为所有请求都已处理 - this.autoIds.clear(); - } - - private int findNextAvailableAutoId() { - // 如果已经用尽 - if (this.currentAutoId > this.maxId) { - return -1; - } - // 寻找下一个可用的id - this.currentAutoId = this.occupiedIds.nextClearBit(this.currentAutoId); - // 已经用完了 - if (this.currentAutoId > maxId) { - return -1; - } - // 找到了 - return this.currentAutoId; - } - - // 强制使用某个id,这时候直接标记到occupiedIds,如果被占用,则直接抛出异常 - public CompletableFuture forceId(final String name, int index) { - // 检查ID是否在有效范围内,一般不会在这触发 - if (index < this.minId || index > this.maxId) { - return CompletableFuture.failedFuture(new AutoIdOutOfRangeException(name, index, this.minId, this.maxId)); - } - - // 检查ID是否已被其他名称占用 - String previous = this.forcedIds.inverse().get(index); - if (previous != null && !previous.equals(name)) { - return CompletableFuture.failedFuture(new AutoIdConflictException(previous, index)); - } - - this.forcedIds.put(name, index); - this.cachedIds.remove(name); // 如果曾经被缓存过,那么移除 - return CompletableFuture.completedFuture(index); - } - - // 自动分配id,优先使用缓存的值 - public CompletableFuture autoId(final String name) { - CompletableFuture future = new CompletableFuture<>(); - this.autoIds.put(name, future); - return future; - } - - // 大多数时候通过指令,移除那些已经不再被使用的id,使用完以后记得调用saveCache以保存更改 - public int clearUnusedIds(Predicate predicate) { - List toRemove = new ArrayList<>(); - for (String id : this.cachedIds.keySet()) { - if (predicate.test(id)) { - toRemove.add(id); - } - } - for (String id : toRemove) { - Integer removedId = this.cachedIds.remove(id); - if (removedId != null) { - // 只有当这个ID不是强制ID时才从occupiedIds中移除 - if (!forcedIds.containsValue(removedId)) { - occupiedIds.clear(removedId); - } - } - } - return toRemove.size(); - } - - // 获取已分配的ID(用于调试或查询) - public Integer getId(String name) { - if (forcedIds.containsKey(name)) { - return forcedIds.get(name); - } - return cachedIds.get(name); - } - - // 获取所有已分配的ID映射 - public Map getAllocatedIds() { - Map result = new HashMap<>(); - result.putAll(forcedIds); - result.putAll(cachedIds); - return Collections.unmodifiableMap(result); - } - - // 检查某个ID是否已被占用 - public boolean isIdOccupied(int id) { - return occupiedIds.get(id); - } - - // 从缓存中加载文件 - public void loadCache() throws IOException { - if (!Files.exists(this.cachePath)) { - return; - } - JsonElement element = GsonHelper.readJsonFile(this.cachePath); - if (element instanceof JsonObject jsonObject) { - for (Map.Entry entry : jsonObject.entrySet()) { - if (entry.getValue() instanceof JsonPrimitive primitive) { - int id = primitive.getAsInt(); - this.cachedIds.put(entry.getKey(), id); - } - } - } - } - - // 保存缓存到文件 - public void saveCache() throws IOException { - FileUtils.createDirectoriesSafe(this.cachePath.getParent()); - GsonHelper.writeJsonFile(GsonHelper.get().toJsonTree(this.cachedIds), this.cachePath); - } - - public static class AutoIdConflictException extends RuntimeException { - public AutoIdConflictException(String previousOwner, int id) { - super("ID " + id + " is already occupied by: " + previousOwner); - } - } - - public static class AutoIdOutOfRangeException extends RuntimeException { - public AutoIdOutOfRangeException(String name, int id, int min, int max) { - super("ID " + id + " for '" + name + "' is out of range. Valid range: " + min + "-" + max); - } - } - - public static class AutoIdExhaustedException extends RuntimeException { - public AutoIdExhaustedException(String name, int min, int max) { - super("No available auto ID for '" + name + "'. All IDs in range " + min + "-" + max + " are occupied."); - } - } -} \ No newline at end of file diff --git a/core/src/main/java/net/momirealms/craftengine/core/pack/cache/IdAllocator.java b/core/src/main/java/net/momirealms/craftengine/core/pack/cache/IdAllocator.java new file mode 100644 index 000000000..9e6d44b2f --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/pack/cache/IdAllocator.java @@ -0,0 +1,226 @@ +package net.momirealms.craftengine.core.pack.cache; + +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; +import net.momirealms.craftengine.core.util.FileUtils; +import net.momirealms.craftengine.core.util.GsonHelper; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; + +public class IdAllocator { + private final Path cacheFilePath; + private final BiMap forcedIdMap = HashBiMap.create(128); + private final Map cachedIdMap = new HashMap<>(); + private final BitSet occupiedIdSet = new BitSet(); + private final Map> pendingAllocations = new HashMap<>(); + + private int nextAutoId; + private int minId; + private int maxId; + + public IdAllocator(Path cacheFilePath) { + this.cacheFilePath = cacheFilePath; + } + + /** + * 重置分配器状态 + * @param minId 最小ID(包含) + * @param maxId 最大ID(包含) + */ + public void reset(int minId, int maxId) { + this.minId = minId; + this.nextAutoId = minId; + this.maxId = maxId; + this.occupiedIdSet.clear(); + this.forcedIdMap.clear(); + this.pendingAllocations.clear(); + this.cachedIdMap.clear(); + } + + /** + * 处理所有待分配的自动ID请求 + */ + public void processPendingAllocations() { + for (Map.Entry> entry : this.pendingAllocations.entrySet()) { + String name = entry.getKey(); + CompletableFuture future = entry.getValue(); + + if (future.isDone()) { + continue; // 不应该发生的情况 + } + + // 优先尝试使用缓存的ID + Integer cachedId = this.cachedIdMap.get(name); + if (isIdAvailable(cachedId)) { + allocateId(name, cachedId, future); + continue; + } + + // 分配新的自动ID + int newId = findNextAvailableId(); + if (newId == -1) { + future.completeExceptionally(new IdExhaustedException(name, this.minId, this.maxId)); + continue; + } + + allocateId(name, newId, future); + this.cachedIdMap.put(name, newId); + } + + this.pendingAllocations.clear(); + } + + private boolean isIdAvailable(Integer id) { + return id != null && id >= this.minId && id <= this.maxId + && !this.occupiedIdSet.get(id); + } + + private void allocateId(String name, int id, CompletableFuture future) { + this.occupiedIdSet.set(id); + future.complete(id); + } + + private int findNextAvailableId() { + if (this.nextAutoId > this.maxId) { + return -1; + } + + this.nextAutoId = this.occupiedIdSet.nextClearBit(this.nextAutoId); + return this.nextAutoId <= this.maxId ? this.nextAutoId : -1; + } + + /** + * 强制分配指定ID,无视限制 + * @param name 名称 + * @param id 要分配的ID + * @return 分配结果的Future + */ + public CompletableFuture assignFixedId(String name, int id) { + // 检查ID是否被其他名称占用 + String existingOwner = this.forcedIdMap.inverse().get(id); + if (existingOwner != null && !existingOwner.equals(name)) { + return CompletableFuture.failedFuture(new IdConflictException(existingOwner, id)); + } + + this.forcedIdMap.put(name, id); + this.cachedIdMap.remove(name); // 清除可能的缓存 + this.occupiedIdSet.set(id); + return CompletableFuture.completedFuture(id); + } + + /** + * 请求自动分配ID + * @param name 名称 + * @return 分配结果的Future + */ + public CompletableFuture requestAutoId(String name) { + CompletableFuture future = new CompletableFuture<>(); + this.pendingAllocations.put(name, future); + return future; + } + + /** + * 清理不再使用的ID + * @param shouldRemove 判断是否应该移除的谓词 + * @return 被移除的ID数量 + */ + public int cleanupUnusedIds(Predicate shouldRemove) { + List idsToRemove = new ArrayList<>(); + for (String id : this.cachedIdMap.keySet()) { + if (shouldRemove.test(id)) { + idsToRemove.add(id); + } + } + + int removedCount = 0; + for (String id : idsToRemove) { + Integer removedId = this.cachedIdMap.remove(id); + if (removedId != null && !this.forcedIdMap.containsValue(removedId)) { + this.occupiedIdSet.clear(removedId); + removedCount++; + } + } + return removedCount; + } + + /** + * 获取指定名称的ID + * @param name 名称 + * @return ID,如果不存在返回null + */ + public Integer getId(String name) { + Integer forcedId = this.forcedIdMap.get(name); + return forcedId != null ? forcedId : this.cachedIdMap.get(name); + } + + /** + * 获取所有已分配的ID映射(不可修改) + */ + public Map getAllAllocatedIds() { + Map result = new HashMap<>(); + result.putAll(this.forcedIdMap); + result.putAll(this.cachedIdMap); + return Collections.unmodifiableMap(result); + } + + /** + * 检查ID是否已被占用 + */ + public boolean isIdOccupied(int id) { + return this.occupiedIdSet.get(id); + } + + /** + * 从文件加载缓存 + */ + public void loadFromCache() throws IOException { + if (!Files.exists(this.cacheFilePath)) { + return; + } + + JsonElement element = GsonHelper.readJsonFile(this.cacheFilePath); + if (element instanceof JsonObject jsonObject) { + for (Map.Entry entry : jsonObject.entrySet()) { + if (entry.getValue() instanceof JsonPrimitive primitive) { + int id = primitive.getAsInt(); + this.cachedIdMap.put(entry.getKey(), id); + } + } + } + } + + /** + * 保存缓存到文件 + */ + public void saveToCache() throws IOException { + FileUtils.createDirectoriesSafe(this.cacheFilePath.getParent()); + GsonHelper.writeJsonFile(GsonHelper.get().toJsonTree(this.cachedIdMap), this.cacheFilePath); + } + + // 异常类保持不变,但建议也重命名以保持一致性 + public static class IdConflictException extends RuntimeException { + public IdConflictException(String previousOwner, int id) { + super("ID " + id + " is already occupied by: " + previousOwner); + } + } + + public static class IdOutOfRangeException extends RuntimeException { + public IdOutOfRangeException(String name, int id, int min, int max) { + super("ID " + id + " for '" + name + "' is out of range. Valid range: " + min + "-" + max); + } + } + + public static class IdExhaustedException extends RuntimeException { + public IdExhaustedException(String name, int min, int max) { + super("No available auto ID for '" + name + "'. All IDs in range " + min + "-" + max + " are occupied."); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/config/Config.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/config/Config.java index c2da59fa2..1cb3d542b 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/plugin/config/Config.java +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/config/Config.java @@ -159,6 +159,8 @@ public class Config { protected boolean item$update_triggers$click_in_inventory; protected boolean item$update_triggers$drop; protected boolean item$update_triggers$pick_up; + protected int item$custom_model_data_starting_value$default; + protected Map item$custom_model_data_starting_value$overrides; protected String equipment$sacrificed_vanilla_armor$type; protected Key equipment$sacrificed_vanilla_armor$asset_id; @@ -239,6 +241,7 @@ public class Config { .addIgnoredRoute(PluginProperties.getValue("config"), "resource-pack.delivery.hosting", '.') .addIgnoredRoute(PluginProperties.getValue("config"), "chunk-system.process-invalid-blocks.convert", '.') .addIgnoredRoute(PluginProperties.getValue("config"), "chunk-system.process-invalid-furniture.convert", '.') + .addIgnoredRoute(PluginProperties.getValue("config"), "item.custom-model-data-starting-value.overrides", '.') .build()); } try { @@ -398,6 +401,22 @@ public class Config { item$update_triggers$click_in_inventory = config.getBoolean("item.update-triggers.click-in-inventory", false); item$update_triggers$drop = config.getBoolean("item.update-triggers.drop", false); item$update_triggers$pick_up = config.getBoolean("item.update-triggers.pick-up", false); + item$custom_model_data_starting_value$default = config.getInt("item.custom-model-data-starting-value.default", 10000); + + Section customModelDataOverridesSection = config.getSection("item.custom-model-data-starting-value.overrides"); + if (customModelDataOverridesSection != null) { + Map customModelDataOverrides = new HashMap<>(); + for (Map.Entry entry : customModelDataOverridesSection.getStringRouteMappedValues(false).entrySet()) { + if (entry.getValue() instanceof String s) { + customModelDataOverrides.put(Key.of(entry.getKey()), Integer.parseInt(s)); + } else if (entry.getValue() instanceof Integer i) { + customModelDataOverrides.put(Key.of(entry.getKey()), i); + } + } + item$custom_model_data_starting_value$overrides = customModelDataOverrides; + } else { + item$custom_model_data_starting_value$overrides = Map.of(); + } // block block$sound_system$enable = config.getBoolean("block.sound-system.enable", true); @@ -752,6 +771,13 @@ public class Config { return instance.furniture$hide_base_entity; } + public static int customModelDataStartingValue(Key material) { + if (instance.item$custom_model_data_starting_value$overrides.containsKey(material)) { + return instance.item$custom_model_data_starting_value$overrides.get(material); + } + return instance.item$custom_model_data_starting_value$default; + } + public static int compressionMethod() { int id = instance.chunk_system$compression_method; if (id <= 0 || id > CompressionMethod.METHOD_COUNT) { diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/config/IdObjectConfigParser.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/config/IdObjectConfigParser.java index 2b2eb5125..9238a31cc 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/plugin/config/IdObjectConfigParser.java +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/config/IdObjectConfigParser.java @@ -8,6 +8,6 @@ import java.nio.file.Path; public interface IdObjectConfigParser extends ConfigParser { - default void parseObject(Pack pack, Path path, Key id, Object object) throws LocalizedException { + default void parseObject(Pack pack, Path path, String node, Key id, Object object) throws LocalizedException { } } diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/config/IdSectionConfigParser.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/config/IdSectionConfigParser.java index b137ebfe6..e14e42ba2 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/plugin/config/IdSectionConfigParser.java +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/config/IdSectionConfigParser.java @@ -9,6 +9,6 @@ import java.util.Map; public interface IdSectionConfigParser extends ConfigParser { - default void parseSection(Pack pack, Path path, Key id, Map section) throws LocalizedException { + default void parseSection(Pack pack, Path path, String node, Key id, Map section) throws LocalizedException { } } diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/config/template/TemplateManagerImpl.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/config/template/TemplateManagerImpl.java index 9d1a505aa..33c2622f4 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/plugin/config/template/TemplateManagerImpl.java +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/config/template/TemplateManagerImpl.java @@ -51,9 +51,9 @@ public class TemplateManagerImpl implements TemplateManager { } @Override - public void parseObject(Pack pack, Path path, Key id, Object obj) { + public void parseObject(Pack pack, Path path, String node, Key id, Object obj) { if (templates.containsKey(id)) { - throw new LocalizedResourceConfigException("warning.config.template.duplicate", path.toString(), id.toString()); + throw new LocalizedResourceConfigException("warning.config.template.duplicate"); } // 预处理会将 string类型的键或值解析为ArgumentString,以加速模板应用。所以处理后不可能存在String类型。 templates.put(id, preprocessUnknownValue(obj)); diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/context/GlobalVariableManager.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/context/GlobalVariableManager.java index d858deed2..ee677e725 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/plugin/context/GlobalVariableManager.java +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/context/GlobalVariableManager.java @@ -6,6 +6,7 @@ import net.momirealms.craftengine.core.plugin.Manageable; import net.momirealms.craftengine.core.plugin.config.ConfigParser; import net.momirealms.craftengine.core.plugin.config.IdObjectConfigParser; import net.momirealms.craftengine.core.plugin.locale.LocalizedException; +import net.momirealms.craftengine.core.util.Key; import org.jetbrains.annotations.Nullable; import java.nio.file.Path; @@ -49,7 +50,7 @@ public class GlobalVariableManager implements Manageable { } @Override - public void parseObject(Pack pack, Path path, net.momirealms.craftengine.core.util.Key id, Object object) throws LocalizedException { + public void parseObject(Pack pack, Path path, String node, Key id, Object object) throws LocalizedException { if (object != null) { GlobalVariableManager.this.globalVariables.put(id.value(), object.toString()); } diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/gui/category/ItemBrowserManagerImpl.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/gui/category/ItemBrowserManagerImpl.java index 7edb1900e..ed34e31a9 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/plugin/gui/category/ItemBrowserManagerImpl.java +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/gui/category/ItemBrowserManagerImpl.java @@ -110,7 +110,7 @@ public class ItemBrowserManagerImpl implements ItemBrowserManager { } @Override - public void parseSection(Pack pack, Path path, Key id, Map section) { + public void parseSection(Pack pack, Path path, String node, Key id, Map section) { String name = section.getOrDefault("name", id).toString(); List members = MiscUtils.getAsStringList(section.getOrDefault("list", List.of())); Key icon = Key.of(section.getOrDefault("icon", ItemKeys.STONE).toString()); diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/locale/LocalizedResourceConfigException.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/locale/LocalizedResourceConfigException.java index 994e4c785..5017b6012 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/plugin/locale/LocalizedResourceConfigException.java +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/locale/LocalizedResourceConfigException.java @@ -37,7 +37,7 @@ public class LocalizedResourceConfigException extends LocalizedException { super.setArgument(0, path.toString()); } - public void setId(String id) { + public void setNode(String id) { super.setArgument(1, id); } } diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/locale/TranslationManagerImpl.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/locale/TranslationManagerImpl.java index f1e119a26..41b8bb69f 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/plugin/locale/TranslationManagerImpl.java +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/locale/TranslationManagerImpl.java @@ -257,10 +257,10 @@ public class TranslationManagerImpl implements TranslationManager { } @Override - public void parseSection(Pack pack, Path path, net.momirealms.craftengine.core.util.Key id, Map section) { + public void parseSection(Pack pack, Path path, String node, net.momirealms.craftengine.core.util.Key id, Map section) { Locale locale = TranslationManager.parseLocale(id.value()); if (locale == null) { - throw new LocalizedResourceConfigException("warning.config.i18n.unknown_locale", path, id); + throw new LocalizedResourceConfigException("warning.config.i18n.unknown_locale"); } Map bundle = new HashMap<>(); @@ -288,7 +288,7 @@ public class TranslationManagerImpl implements TranslationManager { } @Override - public void parseSection(Pack pack, Path path, net.momirealms.craftengine.core.util.Key id, Map section) { + public void parseSection(Pack pack, Path path, String node, net.momirealms.craftengine.core.util.Key id, Map section) { String langId = id.value().toLowerCase(Locale.ENGLISH); Map sectionData = section.entrySet().stream() .collect(Collectors.toMap( diff --git a/core/src/main/java/net/momirealms/craftengine/core/sound/AbstractSoundManager.java b/core/src/main/java/net/momirealms/craftengine/core/sound/AbstractSoundManager.java index 6ca57ab61..2e6def245 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/sound/AbstractSoundManager.java +++ b/core/src/main/java/net/momirealms/craftengine/core/sound/AbstractSoundManager.java @@ -80,7 +80,7 @@ public abstract class AbstractSoundManager implements SoundManager { } @Override - public void parseSection(Pack pack, Path path, Key id, Map section) { + public void parseSection(Pack pack, Path path, String node, Key id, Map section) { if (AbstractSoundManager.this.songs.containsKey(id)) { throw new LocalizedResourceConfigException("warning.config.jukebox_song.duplicate"); } @@ -107,7 +107,7 @@ public abstract class AbstractSoundManager implements SoundManager { } @Override - public void parseSection(Pack pack, Path path, Key id, Map section) { + public void parseSection(Pack pack, Path path, String node, Key id, Map section) { if (AbstractSoundManager.this.byId.containsKey(id)) { throw new LocalizedResourceConfigException("warning.config.sound.duplicate"); } diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/ResourceConfigUtils.java b/core/src/main/java/net/momirealms/craftengine/core/util/ResourceConfigUtils.java index 58c09669e..b1a135387 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/util/ResourceConfigUtils.java +++ b/core/src/main/java/net/momirealms/craftengine/core/util/ResourceConfigUtils.java @@ -1,13 +1,16 @@ package net.momirealms.craftengine.core.util; import com.mojang.datafixers.util.Either; +import net.momirealms.craftengine.core.plugin.CraftEngine; import net.momirealms.craftengine.core.plugin.locale.LocalizedException; import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException; +import net.momirealms.craftengine.core.plugin.locale.TranslationManager; import net.momirealms.craftengine.core.world.Vec3d; import org.jetbrains.annotations.Nullable; import org.joml.Quaternionf; import org.joml.Vector3f; +import java.nio.file.Path; import java.util.*; import java.util.function.Function; import java.util.function.Supplier; @@ -280,4 +283,31 @@ public final class ResourceConfigUtils { } } } + + public static void runCatching(Path configPath, String node, Runnable runnable, Supplier config) { + try { + runnable.run(); + } catch (LocalizedException e) { + printWarningRecursively(e, configPath, node); + } catch (Exception e) { + String message = "Unexpected error loading file " + configPath + " - '" + node + "'."; + if (config != null) { + message += " Configuration details: " + config.get(); + } + CraftEngine.instance().logger().warn(message, e); + } + } + + private static void printWarningRecursively(LocalizedException e, Path path, String node) { + for (Throwable t : e.getSuppressed()) { + if (t instanceof LocalizedException suppressed) { + printWarningRecursively(suppressed, path, node); + } + } + if (e instanceof LocalizedResourceConfigException exception) { + exception.setPath(path); + exception.setNode(node); + } + TranslationManager.instance().log(e.node(), e.arguments()); + } }