diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/api/BukkitAdaptors.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/api/BukkitAdaptors.java
index 6dcd0167a..c667f832e 100644
--- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/api/BukkitAdaptors.java
+++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/api/BukkitAdaptors.java
@@ -3,37 +3,63 @@ package net.momirealms.craftengine.bukkit.api;
import net.momirealms.craftengine.bukkit.entity.BukkitEntity;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.plugin.user.BukkitServerPlayer;
-import net.momirealms.craftengine.bukkit.util.LocationUtils;
import net.momirealms.craftengine.bukkit.world.BukkitExistingBlock;
import net.momirealms.craftengine.bukkit.world.BukkitWorld;
-import net.momirealms.craftengine.core.world.WorldPosition;
-import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
public final class BukkitAdaptors {
private BukkitAdaptors() {}
- public static BukkitServerPlayer adapt(final Player player) {
+ /**
+ * Adapts a Bukkit Player to a CraftEngine BukkitServerPlayer.
+ * This provides access to CraftEngine-specific player functionality and data.
+ *
+ * @param player the Bukkit Player to adapt, must not be null
+ * @return a non-null BukkitServerPlayer instance wrapping the provided player
+ */
+ @NotNull
+ public static BukkitServerPlayer adapt(@NotNull final Player player) {
return BukkitCraftEngine.instance().adapt(player);
}
- public static BukkitWorld adapt(final World world) {
+ /**
+ * Adapts a Bukkit World to a CraftEngine BukkitWorld.
+ * This enables CraftEngine world operations on Bukkit world instances.
+ *
+ * @param world the Bukkit World to adapt, must not be null
+ * @return a non-null BukkitWorld instance wrapping the provided world
+ */
+ @NotNull
+ public static BukkitWorld adapt(@NotNull final World world) {
return new BukkitWorld(world);
}
- public static BukkitEntity adapt(final Entity entity) {
+ /**
+ * Adapts a Bukkit Entity to a CraftEngine BukkitEntity.
+ * This provides CraftEngine entity functionality for Bukkit entities.
+ *
+ * @param entity the Bukkit Entity to adapt, must not be null
+ * @return a non-null BukkitEntity instance wrapping the provided entity
+ */
+ @NotNull
+ public static BukkitEntity adapt(@NotNull final Entity entity) {
return new BukkitEntity(entity);
}
- public static BukkitExistingBlock adapt(final Block block) {
+ /**
+ * Adapts a Bukkit Block to a CraftEngine BukkitExistingBlock.
+ * This enables CraftEngine block operations on Bukkit block instances.
+ *
+ * @param block the Bukkit Block to adapt, must not be null
+ * @return a non-null BukkitExistingBlock instance wrapping the provided block
+ */
+ @NotNull
+ public static BukkitExistingBlock adapt(@NotNull final Block block) {
return new BukkitExistingBlock(block);
}
-
- public static Location toLocation(WorldPosition position) {
- return LocationUtils.toLocation(position);
- }
}
diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/api/CraftEngineImages.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/api/CraftEngineImages.java
new file mode 100644
index 000000000..3a420339f
--- /dev/null
+++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/api/CraftEngineImages.java
@@ -0,0 +1,40 @@
+package net.momirealms.craftengine.bukkit.api;
+
+import net.momirealms.craftengine.bukkit.font.BukkitFontManager;
+import net.momirealms.craftengine.core.font.BitmapImage;
+import net.momirealms.craftengine.core.util.Key;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Map;
+
+public final class CraftEngineImages {
+
+ private CraftEngineImages() {}
+
+ /**
+ * Returns an unmodifiable map of all currently loaded custom images.
+ * The map keys represent unique identifiers, and the values are the corresponding BitmapImage instances.
+ *
+ *
Important: Do not attempt to access this method during the onEnable phase
+ * as it will be empty. Instead, listen for the {@code CraftEngineReloadEvent} and use this method
+ * after the event is fired to obtain the complete image list.
+ *
+ * @return a non-null map containing all loaded custom images
+ */
+ @NotNull
+ public static Map loadedImages() {
+ return BukkitFontManager.instance().loadedImages();
+ }
+
+ /**
+ * Gets a custom image by ID
+ *
+ * @param id id
+ * @return the custom image
+ */
+ @Nullable
+ public static BitmapImage byId(@NotNull Key id) {
+ return BukkitFontManager.instance().loadedImages().get(id);
+ }
+}
diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/BukkitBlockManager.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/BukkitBlockManager.java
index 36c421336..abcf5d08f 100644
--- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/BukkitBlockManager.java
+++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/BukkitBlockManager.java
@@ -44,7 +44,7 @@ public final class BukkitBlockManager extends AbstractBlockManager {
// 事件监听器
private final BlockEventListener blockEventListener;
// 用于缓存string形式的方块状态到原版方块状态
- private final Map blockStateCache = new HashMap<>(1024);
+ private final Map blockStateCache = new HashMap<>(1024);
// 用于临时存储可燃烧自定义方块的列表
private final List burnableBlocks = new ArrayList<>();
// 可燃烧的方块
@@ -169,25 +169,22 @@ public final class BukkitBlockManager extends AbstractBlockManager {
@Override
public BlockStateWrapper createVanillaBlockState(String blockState) {
- Object state = parseBlockState(blockState);
- if (state == null) return null;
- return BlockStateUtils.toBlockStateWrapper(state);
+ return this.blockStateCache.computeIfAbsent(blockState, k -> {
+ Object state = parseBlockState(k);
+ if (state == null) return null;
+ return BlockStateUtils.toBlockStateWrapper(state);
+ });
}
@Nullable
private Object parseBlockState(String state) {
- if (this.blockStateCache.containsKey(state)) {
- return this.blockStateCache.get(state);
- }
try {
Object registryOrLookUp = MBuiltInRegistries.BLOCK;
if (CoreReflections.method$Registry$asLookup != null) {
registryOrLookUp = CoreReflections.method$Registry$asLookup.invoke(registryOrLookUp);
}
Object result = CoreReflections.method$BlockStateParser$parseForBlock.invoke(null, registryOrLookUp, state, false);
- Object resultState = CoreReflections.method$BlockStateParser$BlockResult$blockState.invoke(result);
- this.blockStateCache.put(state, resultState);
- return resultState;
+ return CoreReflections.method$BlockStateParser$BlockResult$blockState.invoke(result);
} catch (Exception e) {
Debugger.BLOCK.warn(() -> "Failed to create block state: " + state, e);
return null;
diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/font/BukkitFontManager.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/font/BukkitFontManager.java
index f6304a758..637fb20a5 100644
--- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/font/BukkitFontManager.java
+++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/font/BukkitFontManager.java
@@ -36,11 +36,17 @@ import java.lang.reflect.InvocationTargetException;
import java.util.*;
public class BukkitFontManager extends AbstractFontManager implements Listener {
+ private static BukkitFontManager instance;
private final BukkitCraftEngine plugin;
public BukkitFontManager(BukkitCraftEngine plugin) {
super(plugin);
this.plugin = plugin;
+ instance = this;
+ }
+
+ public static BukkitFontManager instance() {
+ return instance;
}
@Override
diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/BukkitCraftEngine.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/BukkitCraftEngine.java
index e5a984612..fe991ca7f 100644
--- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/BukkitCraftEngine.java
+++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/BukkitCraftEngine.java
@@ -348,6 +348,11 @@ public class BukkitCraftEngine extends CraftEngine {
return (BukkitPackManager) packManager;
}
+ @Override
+ public BukkitFontManager fontManager() {
+ return (BukkitFontManager) fontManager;
+ }
+
@SuppressWarnings("ResultOfMethodCallIgnored")
@Override
public void saveResource(String resourcePath) {
diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/command/BukkitCommandManager.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/command/BukkitCommandManager.java
index 024b38db9..14c4d9378 100644
--- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/command/BukkitCommandManager.java
+++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/command/BukkitCommandManager.java
@@ -56,7 +56,8 @@ public class BukkitCommandManager extends AbstractCommandManager
new ListResourceCommand(this, plugin),
new UploadPackCommand(this, plugin),
new SendResourcePackCommand(this, plugin),
- new DebugSaveDefaultResourcesCommand(this, plugin)
+ new DebugSaveDefaultResourcesCommand(this, plugin),
+ new DebugCleanCacheCommand(this, plugin)
));
final LegacyPaperCommandManager manager = (LegacyPaperCommandManager) getCommandManager();
manager.settings().set(ManagerSetting.ALLOW_UNSAFE_REGISTRATION, true);
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
new file mode 100644
index 000000000..8337140f6
--- /dev/null
+++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/command/feature/DebugCleanCacheCommand.java
@@ -0,0 +1,84 @@
+package net.momirealms.craftengine.bukkit.plugin.command.feature;
+
+import net.momirealms.craftengine.bukkit.api.CraftEngineItems;
+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.pack.allocator.IdAllocator;
+import net.momirealms.craftengine.core.plugin.CraftEngine;
+import net.momirealms.craftengine.core.plugin.command.CraftEngineCommandManager;
+import net.momirealms.craftengine.core.util.Key;
+import org.bukkit.command.CommandSender;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.incendo.cloud.Command;
+import org.incendo.cloud.context.CommandContext;
+import org.incendo.cloud.context.CommandInput;
+import org.incendo.cloud.parser.standard.StringParser;
+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.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+
+public class DebugCleanCacheCommand extends BukkitCommandFeature {
+
+ public DebugCleanCacheCommand(CraftEngineCommandManager commandManager, CraftEngine plugin) {
+ super(commandManager, plugin);
+ }
+
+ @Override
+ public Command.Builder extends CommandSender> assembleCommand(org.incendo.cloud.CommandManager manager, Command.Builder builder) {
+ return builder
+ .required("type", StringParser.stringComponent().suggestionProvider(new SuggestionProvider<>() {
+ @Override
+ public @NonNull CompletableFuture extends @NonNull Iterable extends @NonNull Suggestion>> suggestionsFuture(@NonNull CommandContext context, @NonNull CommandInput input) {
+ return CompletableFuture.completedFuture(List.of(Suggestion.suggestion("custom-model-data"), Suggestion.suggestion("custom-block-states"), Suggestion.suggestion("visual-block-states"), Suggestion.suggestion("font")));
+ }
+ }))
+ .handler(context -> {
+ if (this.plugin().isReloading()) {
+ context.sender().sendMessage("The plugin is reloading. Please wait until the process is complete.");
+ return;
+ }
+ String type = context.get("type");
+ switch (type) {
+ case "custom-model-data" -> {
+ BukkitItemManager instance = BukkitItemManager.instance();
+ Set ids = CraftEngineItems.loadedItems().keySet().stream().map(Key::toString).collect(Collectors.toSet());
+ int total = 0;
+ for (Map.Entry entry : instance.itemParser().idAllocators().entrySet()) {
+ List removed = entry.getValue().cleanupUnusedIds(i -> !ids.contains(i));
+ total += removed.size();
+ try {
+ entry.getValue().saveToCache();
+ } catch (IOException e) {
+ this.plugin().logger().warn("Error while saving custom model data allocation for material " + entry.getKey().asString(), e);
+ return;
+ }
+ for (String id : removed) {
+ this.plugin().logger().info("Cleaned unused 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();
+
+ }
+ }
+ });
+ }
+
+ @Override
+ public String getFeatureID() {
+ return "debug_clean_cache";
+ }
+}
diff --git a/common-files/src/main/resources/commands.yml b/common-files/src/main/resources/commands.yml
index 3a2b6c8de..9ba12ceeb 100644
--- a/common-files/src/main/resources/commands.yml
+++ b/common-files/src/main/resources/commands.yml
@@ -210,6 +210,13 @@ debug_save_default_resources:
- /craftengine debug save-default-resources
- /ce debug save-default-resources
+debug_clean_cache:
+ enable: true
+ permission: ce.command.debug.clean_cache
+ usage:
+ - /craftengine debug clean-cache
+ - /ce debug clean-cache
+
debug_test:
enable: true
permission: ce.command.debug.test
diff --git a/common-files/src/main/resources/resources/default/configuration/blocks/chessboard_block.yml b/common-files/src/main/resources/resources/default/configuration/blocks/chessboard_block.yml
index 07e6a3f1e..7e36d6d45 100644
--- a/common-files/src/main/resources/resources/default/configuration/blocks/chessboard_block.yml
+++ b/common-files/src/main/resources/resources/default/configuration/blocks/chessboard_block.yml
@@ -34,7 +34,7 @@ items:
default: north
appearances:
east:
- state: note_block:18
+ auto-state: solid
model:
path: minecraft:block/custom/chessboard_block
y: 270
@@ -43,16 +43,16 @@ items:
textures:
pattern: minecraft:block/custom/chessboard_block
north:
- state: note_block:19
+ auto-state: solid
model:
path: minecraft:block/custom/chessboard_block
y: 180
south:
- state: note_block:20
+ auto-state: solid
model:
path: minecraft:block/custom/chessboard_block
west:
- state: note_block:21
+ auto-state: solid
model:
path: minecraft:block/custom/chessboard_block
y: 90
diff --git a/common-files/src/main/resources/resources/default/configuration/blocks/chinese_lantern.yml b/common-files/src/main/resources/resources/default/configuration/blocks/chinese_lantern.yml
index 4776b2d4d..420809bc8 100644
--- a/common-files/src/main/resources/resources/default/configuration/blocks/chinese_lantern.yml
+++ b/common-files/src/main/resources/resources/default/configuration/blocks/chinese_lantern.yml
@@ -25,7 +25,7 @@ items:
luminance: 15
map-color: 36
state:
- state: note_block:15
+ auto-state: solid
model:
path: minecraft:block/custom/chinese_lantern
generation:
diff --git a/common-files/src/main/resources/resources/default/configuration/blocks/copper_coil.yml b/common-files/src/main/resources/resources/default/configuration/blocks/copper_coil.yml
index 94185e5aa..9d9b8bf83 100644
--- a/common-files/src/main/resources/resources/default/configuration/blocks/copper_coil.yml
+++ b/common-files/src/main/resources/resources/default/configuration/blocks/copper_coil.yml
@@ -36,7 +36,7 @@ items:
default: false
appearances:
off:
- state: cactus:0
+ auto-state: cactus
model:
path: minecraft:block/custom/copper_coil
generation:
@@ -47,7 +47,7 @@ items:
top: minecraft:block/custom/copper_coil
side: minecraft:block/custom/copper_coil_side
on:
- state: cactus:1
+ auto-state: cactus
model:
path: minecraft:block/custom/copper_coil_on
generation:
diff --git a/common-files/src/main/resources/resources/default/configuration/blocks/ender_pearl_flower.yml b/common-files/src/main/resources/resources/default/configuration/blocks/ender_pearl_flower.yml
index ccb1fad33..0b61f7161 100644
--- a/common-files/src/main/resources/resources/default/configuration/blocks/ender_pearl_flower.yml
+++ b/common-files/src/main/resources/resources/default/configuration/blocks/ender_pearl_flower.yml
@@ -90,7 +90,7 @@ blocks:
range: 0~2
appearances:
stage_0:
- state: tripwire:1
+ auto-state: lower_tripwire
models:
- path: minecraft:block/custom/ender_pearl_flower_stage_0
generation:
@@ -98,7 +98,7 @@ blocks:
textures:
cross: minecraft:block/custom/ender_pearl_flower_stage_0
stage_1:
- state: tripwire:0
+ auto-state: higher_tripwire
models:
- path: minecraft:block/custom/ender_pearl_flower_stage_1
generation:
@@ -106,7 +106,7 @@ blocks:
textures:
cross: minecraft:block/custom/ender_pearl_flower_stage_1
stage_2:
- state: sugar_cane:3
+ auto-state: sugar_cane
models:
- path: minecraft:block/custom/ender_pearl_flower_stage_2
generation:
diff --git a/common-files/src/main/resources/resources/default/configuration/blocks/fairy_flower.yml b/common-files/src/main/resources/resources/default/configuration/blocks/fairy_flower.yml
index 5e2b00ba4..52a2815fd 100644
--- a/common-files/src/main/resources/resources/default/configuration/blocks/fairy_flower.yml
+++ b/common-files/src/main/resources/resources/default/configuration/blocks/fairy_flower.yml
@@ -26,7 +26,7 @@ items:
loot:
template: default:loot_table/self
state:
- state: sugar_cane:0
+ auto-state: sugar_cane
models:
- path: minecraft:block/custom/fairy_flower_1
weight: 100
diff --git a/common-files/src/main/resources/resources/default/configuration/blocks/flame_cane.yml b/common-files/src/main/resources/resources/default/configuration/blocks/flame_cane.yml
index f24859eef..4213b62bd 100644
--- a/common-files/src/main/resources/resources/default/configuration/blocks/flame_cane.yml
+++ b/common-files/src/main/resources/resources/default/configuration/blocks/flame_cane.yml
@@ -53,7 +53,7 @@ items:
range: 0~5
appearances:
default:
- state: sugar_cane:2
+ auto-state: sugar_cane
models:
- path: minecraft:block/custom/flame_cane_1
weight: 1
diff --git a/common-files/src/main/resources/resources/default/configuration/blocks/gunpowder_block.yml b/common-files/src/main/resources/resources/default/configuration/blocks/gunpowder_block.yml
index 17ccff09d..e114b183c 100644
--- a/common-files/src/main/resources/resources/default/configuration/blocks/gunpowder_block.yml
+++ b/common-files/src/main/resources/resources/default/configuration/blocks/gunpowder_block.yml
@@ -27,7 +27,7 @@ items:
instrument: snare
map-color: 45
state:
- state: note_block:16
+ auto-state: solid
model:
path: minecraft:block/custom/gunpowder_block
generation:
@@ -59,7 +59,7 @@ items:
instrument: basedrum
map-color: 45
state:
- state: note_block:17
+ auto-state: solid
model:
path: minecraft:block/custom/solid_gunpowder_block
generation:
diff --git a/common-files/src/main/resources/resources/default/configuration/blocks/palm_tree.yml b/common-files/src/main/resources/resources/default/configuration/blocks/palm_tree.yml
index 61ccd87bc..6002d41ac 100644
--- a/common-files/src/main/resources/resources/default/configuration/blocks/palm_tree.yml
+++ b/common-files/src/main/resources/resources/default/configuration/blocks/palm_tree.yml
@@ -27,7 +27,6 @@ items:
states:
template: default:block_state/pillar
arguments:
- base_block: note_block
texture_top_path: minecraft:block/custom/palm_log_top
texture_side_path: minecraft:block/custom/palm_log
model_vertical_path: minecraft:block/custom/palm_log
@@ -57,7 +56,6 @@ items:
states:
template: default:block_state/pillar
arguments:
- base_block: note_block
texture_top_path: minecraft:block/custom/stripped_palm_log_top
texture_side_path: minecraft:block/custom/stripped_palm_log
model_vertical_path: minecraft:block/custom/stripped_palm_log
@@ -90,7 +88,6 @@ items:
states:
template: default:block_state/pillar
arguments:
- base_block: note_block
texture_top_path: minecraft:block/custom/palm_log
texture_side_path: minecraft:block/custom/palm_log
model_vertical_path: minecraft:block/custom/palm_wood
@@ -120,7 +117,6 @@ items:
states:
template: default:block_state/pillar
arguments:
- base_block: note_block
texture_top_path: minecraft:block/custom/stripped_palm_log
texture_side_path: minecraft:block/custom/stripped_palm_log
model_vertical_path: minecraft:block/custom/stripped_palm_wood
@@ -151,7 +147,7 @@ items:
template: default:model/simplified_cube_all
arguments:
path: minecraft:block/custom/palm_planks
- state: note_block:12
+ auto-state: solid
default:palm_sapling:
material: nether_brick
settings:
@@ -187,7 +183,7 @@ items:
range: 0~1
appearances:
default:
- state: oak_sapling:0
+ auto-state: sapling
model:
path: minecraft:block/custom/palm_sapling
generation:
diff --git a/common-files/src/main/resources/resources/default/configuration/blocks/pebble.yml b/common-files/src/main/resources/resources/default/configuration/blocks/pebble.yml
index 5a164c44e..23fe24d71 100644
--- a/common-files/src/main/resources/resources/default/configuration/blocks/pebble.yml
+++ b/common-files/src/main/resources/resources/default/configuration/blocks/pebble.yml
@@ -58,7 +58,7 @@ items:
default: 1
appearances:
one:
- state: tripwire:2
+ auto-state: lower_tripwire
models:
- path: minecraft:block/custom/pebble_1
weight: 1
@@ -72,7 +72,7 @@ items:
weight: 1
y: 270
two:
- state: tripwire:3
+ auto-state: lower_tripwire
models:
- path: minecraft:block/custom/pebble_2
weight: 1
@@ -86,7 +86,7 @@ items:
weight: 1
y: 270
three:
- state: tripwire:4
+ auto-state: higher_tripwire
models:
- path: minecraft:block/custom/pebble_3
weight: 1
diff --git a/common-files/src/main/resources/resources/default/configuration/blocks/reed.yml b/common-files/src/main/resources/resources/default/configuration/blocks/reed.yml
index c43425c81..de1e00be9 100644
--- a/common-files/src/main/resources/resources/default/configuration/blocks/reed.yml
+++ b/common-files/src/main/resources/resources/default/configuration/blocks/reed.yml
@@ -25,7 +25,7 @@ items:
loot:
template: default:loot_table/self
state:
- state: sugar_cane:1
+ auto-state: sugar_cane
model:
path: minecraft:block/custom/reed
recipes:
diff --git a/common-files/src/main/resources/resources/default/configuration/blocks/safe_block.yml b/common-files/src/main/resources/resources/default/configuration/blocks/safe_block.yml
index 710804ef9..63b13e353 100644
--- a/common-files/src/main/resources/resources/default/configuration/blocks/safe_block.yml
+++ b/common-files/src/main/resources/resources/default/configuration/blocks/safe_block.yml
@@ -48,7 +48,7 @@ items:
default: false
appearances:
east:
- state: note_block:22
+ auto-state: note_block
model:
path: minecraft:block/custom/safe_block
y: 90
@@ -59,7 +59,7 @@ items:
side: minecraft:block/custom/safe_block_side
top: minecraft:block/custom/safe_block_top
east_open:
- state: note_block:23
+ auto-state: note_block
model:
path: minecraft:block/custom/safe_block_open
y: 90
@@ -70,30 +70,30 @@ items:
side: minecraft:block/custom/safe_block_side
top: minecraft:block/custom/safe_block_top
north:
- state: note_block:24
+ auto-state: note_block
model:
path: minecraft:block/custom/safe_block
north_open:
- state: note_block:25
+ auto-state: note_block
model:
path: minecraft:block/custom/safe_block_open
south:
- state: note_block:26
+ auto-state: note_block
model:
path: minecraft:block/custom/safe_block
y: 180
south_open:
- state: note_block:27
+ auto-state: note_block
model:
path: minecraft:block/custom/safe_block_open
y: 180
west:
- state: note_block:28
+ auto-state: note_block
model:
path: minecraft:block/custom/safe_block
y: 270
west_open:
- state: note_block:29
+ auto-state: note_block
model:
path: minecraft:block/custom/safe_block_open
y: 270
diff --git a/common-files/src/main/resources/resources/default/configuration/blocks/topaz_ore.yml b/common-files/src/main/resources/resources/default/configuration/blocks/topaz_ore.yml
index 0eee914e4..6721d3027 100644
--- a/common-files/src/main/resources/resources/default/configuration/blocks/topaz_ore.yml
+++ b/common-files/src/main/resources/resources/default/configuration/blocks/topaz_ore.yml
@@ -50,7 +50,7 @@ blocks:
arguments:
break_power: 2
state:
- state: note_block:13
+ auto-state: solid
model:
template: default:model/simplified_cube_all
arguments:
diff --git a/common-files/src/main/resources/resources/default/configuration/templates.yml b/common-files/src/main/resources/resources/default/configuration/templates.yml
index 5912b31f5..da609c8b0 100644
--- a/common-files/src/main/resources/resources/default/configuration/templates.yml
+++ b/common-files/src/main/resources/resources/default/configuration/templates.yml
@@ -862,7 +862,7 @@ templates#block_states:
default: y
appearances:
axisY:
- state: ${base_block}
+ auto-state: solid
model:
path: ${model_vertical_path}
generation:
@@ -871,7 +871,7 @@ templates#block_states:
end: ${texture_top_path}
side: ${texture_side_path}
axisX:
- state: ${base_block}
+ auto-state: solid
model:
x: 90
y: 90
@@ -882,7 +882,7 @@ templates#block_states:
end: ${texture_top_path}
side: ${texture_side_path}
axisZ:
- state: ${base_block}
+ auto-state: solid
model:
x: 90
path: ${model_horizontal_path}
@@ -913,7 +913,7 @@ templates#block_states:
range: 1~7
appearances:
default:
- state: ${default_state}
+ auto-state: leaves
model:
path: ${model_path}
generation:
@@ -921,7 +921,7 @@ templates#block_states:
textures:
all: ${texture_path}
waterlogged:
- state: ${waterlogged_state}
+ auto-state: waterlogged_leaves
model:
path: ${model_path}
variants:
@@ -2231,6 +2231,11 @@ templates#block_states:
x: 180
y: 180
variants:
+ waterlogged=true:
+ settings:
+ resistance: 1200.0
+ burnable: false
+ fluid-state: water
facing=east,half=bottom,shape=inner_left,waterlogged=false:
appearance: facing=east,half=bottom,shape=inner_left,waterlogged=false
facing=east,half=bottom,shape=inner_right,waterlogged=false:
@@ -2313,244 +2318,85 @@ templates#block_states:
appearance: facing=west,half=top,shape=straight,waterlogged=false
facing=east,half=bottom,shape=inner_left,waterlogged=true:
appearance: facing=east,half=bottom,shape=inner_left,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=east,half=bottom,shape=inner_right,waterlogged=true:
appearance: facing=east,half=bottom,shape=inner_right,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=east,half=bottom,shape=outer_left,waterlogged=true:
appearance: facing=east,half=bottom,shape=outer_left,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=east,half=bottom,shape=outer_right,waterlogged=true:
appearance: facing=east,half=bottom,shape=outer_right,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=east,half=bottom,shape=straight,waterlogged=true:
appearance: facing=east,half=bottom,shape=straight,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=east,half=top,shape=inner_left,waterlogged=true:
appearance: facing=east,half=top,shape=inner_left,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=east,half=top,shape=inner_right,waterlogged=true:
appearance: facing=east,half=top,shape=inner_right,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=east,half=top,shape=outer_left,waterlogged=true:
appearance: facing=east,half=top,shape=outer_left,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=east,half=top,shape=outer_right,waterlogged=true:
appearance: facing=east,half=top,shape=outer_right,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=east,half=top,shape=straight,waterlogged=true:
appearance: facing=east,half=top,shape=straight,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=north,half=bottom,shape=inner_left,waterlogged=true:
appearance: facing=north,half=bottom,shape=inner_left,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=north,half=bottom,shape=inner_right,waterlogged=true:
appearance: facing=north,half=bottom,shape=inner_right,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=north,half=bottom,shape=outer_left,waterlogged=true:
appearance: facing=north,half=bottom,shape=outer_left,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=north,half=bottom,shape=outer_right,waterlogged=true:
appearance: facing=north,half=bottom,shape=outer_right,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=north,half=bottom,shape=straight,waterlogged=true:
appearance: facing=north,half=bottom,shape=straight,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=north,half=top,shape=inner_left,waterlogged=true:
appearance: facing=north,half=top,shape=inner_left,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=north,half=top,shape=inner_right,waterlogged=true:
appearance: facing=north,half=top,shape=inner_right,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=north,half=top,shape=outer_left,waterlogged=true:
appearance: facing=north,half=top,shape=outer_left,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=north,half=top,shape=outer_right,waterlogged=true:
appearance: facing=north,half=top,shape=outer_right,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=north,half=top,shape=straight,waterlogged=true:
appearance: facing=north,half=top,shape=straight,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=south,half=bottom,shape=inner_left,waterlogged=true:
appearance: facing=south,half=bottom,shape=inner_left,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=south,half=bottom,shape=inner_right,waterlogged=true:
appearance: facing=south,half=bottom,shape=inner_right,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=south,half=bottom,shape=outer_left,waterlogged=true:
appearance: facing=south,half=bottom,shape=outer_left,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=south,half=bottom,shape=outer_right,waterlogged=true:
appearance: facing=south,half=bottom,shape=outer_right,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=south,half=bottom,shape=straight,waterlogged=true:
appearance: facing=south,half=bottom,shape=straight,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=south,half=top,shape=inner_left,waterlogged=true:
appearance: facing=south,half=top,shape=inner_left,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=south,half=top,shape=inner_right,waterlogged=true:
appearance: facing=south,half=top,shape=inner_right,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=south,half=top,shape=outer_left,waterlogged=true:
appearance: facing=south,half=top,shape=outer_left,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=south,half=top,shape=outer_right,waterlogged=true:
appearance: facing=south,half=top,shape=outer_right,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=south,half=top,shape=straight,waterlogged=true:
appearance: facing=south,half=top,shape=straight,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=west,half=bottom,shape=inner_left,waterlogged=true:
appearance: facing=west,half=bottom,shape=inner_left,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=west,half=bottom,shape=inner_right,waterlogged=true:
appearance: facing=west,half=bottom,shape=inner_right,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=west,half=bottom,shape=outer_left,waterlogged=true:
appearance: facing=west,half=bottom,shape=outer_left,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=west,half=bottom,shape=outer_right,waterlogged=true:
appearance: facing=west,half=bottom,shape=outer_right,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=west,half=bottom,shape=straight,waterlogged=true:
appearance: facing=west,half=bottom,shape=straight,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=west,half=top,shape=inner_left,waterlogged=true:
appearance: facing=west,half=top,shape=inner_left,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=west,half=top,shape=inner_right,waterlogged=true:
appearance: facing=west,half=top,shape=inner_right,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=west,half=top,shape=outer_left,waterlogged=true:
appearance: facing=west,half=top,shape=outer_left,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=west,half=top,shape=outer_right,waterlogged=true:
appearance: facing=west,half=top,shape=outer_right,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
facing=west,half=top,shape=straight,waterlogged=true:
appearance: facing=west,half=top,shape=straight,waterlogged=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
+
# pressure plate block
default:block_state/pressure_plate:
properties:
@@ -3171,6 +3017,11 @@ templates#block_states:
- item: ${fence_side_item}
rotation: 270
variants:
+ waterlogged=true:
+ settings:
+ resistance: 1200.0
+ burnable: false
+ fluid-state: water
east=false,north=false,south=false,waterlogged=false,west=false:
appearance: east=false,north=false,south=false,waterlogged=false,west=false
east=true,north=false,south=false,waterlogged=false,west=false:
@@ -3205,100 +3056,37 @@ templates#block_states:
appearance: east=true,north=true,south=true,waterlogged=false,west=true
east=false,north=false,south=false,waterlogged=true,west=false:
appearance: east=false,north=false,south=false,waterlogged=true,west=false
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
east=true,north=false,south=false,waterlogged=true,west=false:
appearance: east=true,north=false,south=false,waterlogged=true,west=false
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
east=false,north=true,south=false,waterlogged=true,west=false:
appearance: east=false,north=true,south=false,waterlogged=true,west=false
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
east=false,north=false,south=true,waterlogged=true,west=false:
appearance: east=false,north=false,south=true,waterlogged=true,west=false
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
east=false,north=false,south=false,waterlogged=true,west=true:
appearance: east=false,north=false,south=false,waterlogged=true,west=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
east=true,north=true,south=false,waterlogged=true,west=false:
appearance: east=true,north=true,south=false,waterlogged=true,west=false
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
east=true,north=false,south=true,waterlogged=true,west=false:
appearance: east=true,north=false,south=true,waterlogged=true,west=false
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
east=true,north=false,south=false,waterlogged=true,west=true:
appearance: east=true,north=false,south=false,waterlogged=true,west=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
east=false,north=true,south=true,waterlogged=true,west=false:
appearance: east=false,north=true,south=true,waterlogged=true,west=false
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
east=false,north=true,south=false,waterlogged=true,west=true:
appearance: east=false,north=true,south=false,waterlogged=true,west=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
east=false,north=false,south=true,waterlogged=true,west=true:
appearance: east=false,north=false,south=true,waterlogged=true,west=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
east=true,north=true,south=true,waterlogged=true,west=false:
appearance: east=true,north=true,south=true,waterlogged=true,west=false
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
east=true,north=true,south=false,waterlogged=true,west=true:
appearance: east=true,north=true,south=false,waterlogged=true,west=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
east=true,north=false,south=true,waterlogged=true,west=true:
appearance: east=true,north=false,south=true,waterlogged=true,west=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
east=false,north=true,south=true,waterlogged=true,west=true:
appearance: east=false,north=true,south=true,waterlogged=true,west=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
east=true,north=true,south=true,waterlogged=true,west=true:
appearance: east=true,north=true,south=true,waterlogged=true,west=true
- settings:
- resistance: 1200.0
- burnable: false
- fluid-state: water
+
# recipes
templates#recipes:
default:recipe/planks:
diff --git a/common-files/src/main/resources/translations/en.yml b/common-files/src/main/resources/translations/en.yml
index 5679ea63c..acd02e1a4 100644
--- a/common-files/src/main/resources/translations/en.yml
+++ b/common-files/src/main/resources/translations/en.yml
@@ -277,6 +277,8 @@ warning.config.block.state.entity_renderer.better_model.missing_model: "
warning.config.block.state.entity_renderer.model_engine.missing_model: "Issue found in file - The block '' is missing the required 'model' argument for 'model_engine' entity renderer. "
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.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 5d5b66231..8f0af28ff 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
@@ -18,9 +18,9 @@ import net.momirealms.craftengine.core.pack.LoadingSequence;
import net.momirealms.craftengine.core.pack.Pack;
import net.momirealms.craftengine.core.pack.PendingConfigSection;
import net.momirealms.craftengine.core.pack.ResourceLocation;
-import net.momirealms.craftengine.core.pack.allocator.BlockStateAllocator;
import net.momirealms.craftengine.core.pack.allocator.BlockStateCandidate;
import net.momirealms.craftengine.core.pack.allocator.IdAllocator;
+import net.momirealms.craftengine.core.pack.allocator.VisualBlockStateAllocator;
import net.momirealms.craftengine.core.pack.model.generation.AbstractModelGenerator;
import net.momirealms.craftengine.core.pack.model.generation.ModelGeneration;
import net.momirealms.craftengine.core.plugin.CraftEngine;
@@ -60,8 +60,6 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
protected final List cachedSuggestions = new ArrayList<>();
// 缓存的使用中的命名空间
protected final Set namespacesInUse = new HashSet<>();
- // 用于检测单个外观方块状态是否被绑定了不同模型
- protected final Map tempVanillaBlockStateModels = new Int2ObjectOpenHashMap<>();
// Map<方块类型, Map<方块状态NBT,模型>>,用于生成block state json
protected final Map> blockStateOverrides = new HashMap<>();
// 用于生成mod使用的block state json
@@ -81,7 +79,9 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
// 自定义状态列表,会随着重载变化
protected final ImmutableBlockState[] immutableBlockStates;
// 倒推缓存
- protected final BlockStateCandidate[] reversedBlockStateArranger;
+ protected final BlockStateCandidate[] autoVisualBlockStateCandidates;
+ // 用于检测单个外观方块状态是否被绑定了不同模型
+ protected final JsonElement[] tempVanillaBlockStateModels;
// 临时存储哪些视觉方块被使用了
protected final Set tempVisualBlockStatesInUse = new HashSet<>();
protected final Set tempVisualBlocksInUse = new HashSet<>();
@@ -91,14 +91,15 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
protected AbstractBlockManager(CraftEngine plugin, int vanillaBlockStateCount, int customBlockCount) {
super(plugin);
this.vanillaBlockStateCount = vanillaBlockStateCount;
- this.blockParser = new BlockParser();
- this.blockStateMappingParser = new BlockStateMappingParser();
this.customBlocks = new DelegatingBlock[customBlockCount];
this.customBlockHolders = new Object[customBlockCount];
this.customBlockStates = new DelegatingBlockState[customBlockCount];
this.immutableBlockStates = new ImmutableBlockState[customBlockCount];
this.blockStateMappings = new int[customBlockCount + vanillaBlockStateCount];
- this.reversedBlockStateArranger = new BlockStateCandidate[vanillaBlockStateCount];
+ this.autoVisualBlockStateCandidates = new BlockStateCandidate[vanillaBlockStateCount];
+ this.tempVanillaBlockStateModels = new JsonElement[vanillaBlockStateCount];
+ this.blockParser = new BlockParser(this.autoVisualBlockStateCandidates);
+ this.blockStateMappingParser = new BlockStateMappingParser();
Arrays.fill(this.blockStateMappings, -1);
}
@@ -130,7 +131,10 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
this.appearanceToRealState.clear();
Arrays.fill(this.blockStateMappings, -1);
Arrays.fill(this.immutableBlockStates, EmptyBlock.STATE);
- Arrays.fill(this.reversedBlockStateArranger, null);
+ Arrays.fill(this.autoVisualBlockStateCandidates, null);
+ for (AutoStateGroup autoStateGroup : AutoStateGroup.values()) {
+ autoStateGroup.reset();
+ }
}
@Override
@@ -188,7 +192,7 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
}
protected void clearCache() {
- this.tempVanillaBlockStateModels.clear();
+ Arrays.fill(this.tempVanillaBlockStateModels, null);
this.tempVisualBlockStatesInUse.clear();
this.tempVisualBlocksInUse.clear();
}
@@ -275,26 +279,10 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
Key blockOwnerId = getBlockOwnerId(beforeState);
List blockStateWrappers = AbstractBlockManager.this.blockStateArranger.computeIfAbsent(blockOwnerId, k -> new ArrayList<>());
blockStateWrappers.add(beforeState);
- AbstractBlockManager.this.reversedBlockStateArranger[beforeState.registryId()] = blockParser.createVisualBlockCandidate(beforeState);
-
+ AbstractBlockManager.this.autoVisualBlockStateCandidates[beforeState.registryId()] = createVisualBlockCandidate(beforeState);
}
exceptionCollector.throwIfPresent();
}
- }
-
- public class BlockParser extends IdSectionConfigParser {
- public static final String[] CONFIG_SECTION_NAME = new String[]{"blocks", "block"};
- private final IdAllocator internalIdAllocator;
- private final List pendingConfigSections = new ArrayList<>();
- private final BlockStateAllocator[] visualBlockStateAllocators = new BlockStateAllocator[AutoStateGroup.values().length];
-
- public BlockParser() {
- this.internalIdAllocator = new IdAllocator(AbstractBlockManager.this.plugin.dataFolderPath().resolve("cache").resolve("custom-block-states.json"));
- }
-
- public void addPendingConfigSection(PendingConfigSection section) {
- this.pendingConfigSections.add(section);
- }
@Nullable
public BlockStateCandidate createVisualBlockCandidate(BlockStateWrapper blockState) {
@@ -302,40 +290,58 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
if (!groups.isEmpty()) {
BlockStateCandidate candidate = new BlockStateCandidate(blockState);
for (AutoStateGroup group : groups) {
- getOrCreateBlockStateAllocator(group).addCandidate(candidate);
+ group.addCandidate(candidate);
}
return candidate;
}
return null;
}
+ }
- private BlockStateAllocator getOrCreateBlockStateAllocator(AutoStateGroup group) {
- int index = group.ordinal();
- BlockStateAllocator visualBlockStateAllocator = this.visualBlockStateAllocators[index];
- if (visualBlockStateAllocator == null) {
- visualBlockStateAllocator = new BlockStateAllocator();
- this.visualBlockStateAllocators[index] = visualBlockStateAllocator;
- }
- return visualBlockStateAllocator;
+ public class BlockParser extends IdSectionConfigParser {
+ public static final String[] CONFIG_SECTION_NAME = new String[]{"blocks", "block"};
+ private final IdAllocator internalIdAllocator;
+ private final VisualBlockStateAllocator visualBlockStateAllocator;
+ private final List pendingConfigSections = new ArrayList<>();
+
+ public BlockParser(BlockStateCandidate[] candidates) {
+ this.internalIdAllocator = new IdAllocator(AbstractBlockManager.this.plugin.dataFolderPath().resolve("cache").resolve("custom-block-states.json"));
+ this.visualBlockStateAllocator = new VisualBlockStateAllocator(AbstractBlockManager.this.plugin.dataFolderPath().resolve("cache").resolve("visual-block-states.json"), candidates, AbstractBlockManager.this::createVanillaBlockState);
+ }
+
+ public void addPendingConfigSection(PendingConfigSection section) {
+ this.pendingConfigSections.add(section);
}
@Override
public void postProcess() {
+ this.visualBlockStateAllocator.processPendingAllocations();
+ try {
+ this.visualBlockStateAllocator.saveToCache();
+ } catch (IOException e) {
+ AbstractBlockManager.this.plugin.logger().warn("Error while saving visual block states allocation", e);
+ }
this.internalIdAllocator.processPendingAllocations();
try {
this.internalIdAllocator.saveToCache();
} catch (IOException e) {
- AbstractBlockManager.this.plugin.logger().warn("Error while saving custom block state allocation", e);
+ AbstractBlockManager.this.plugin.logger().warn("Error while saving custom block states allocation", e);
}
}
@Override
public void preProcess() {
+ this.visualBlockStateAllocator.reset();
+ try {
+ this.visualBlockStateAllocator.loadFromCache();
+ } catch (IOException e) {
+ AbstractBlockManager.this.plugin.logger().warn("Error while loading visual block states allocation cache", e);
+ }
this.internalIdAllocator.reset(0, Config.serverSideBlocks() - 1);
try {
this.internalIdAllocator.loadFromCache();
} catch (IOException e) {
- AbstractBlockManager.this.plugin.logger().warn("Error while loading custom block state allocation cache", e);
+ AbstractBlockManager.this.plugin.logger().warn("Error while loading custom block states allocation cache", e);
}
for (PendingConfigSection section : this.pendingConfigSections) {
ResourceConfigUtils.runCatching(
@@ -437,9 +443,9 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
}
}
- CompletableFutures.allOf(internalIdAllocators).whenComplete((v, t) -> ResourceConfigUtils.runCatching(path, node, () -> {
- if (t != null) {
- if (t instanceof CompletionException e) {
+ CompletableFutures.allOf(internalIdAllocators).whenComplete((v1, t1) -> ResourceConfigUtils.runCatching(path, node, () -> {
+ if (t1 != null) {
+ if (t1 instanceof CompletionException e) {
Throwable cause = e.getCause();
// 这里不会有conflict了,因为之前已经判断过了
if (cause instanceof IdAllocator.IdExhaustedException) {
@@ -449,7 +455,7 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
return;
}
}
- throw new RuntimeException("Unknown error occurred", t);
+ throw new RuntimeException("Unknown error occurred", t1);
}
for (int i = 0; i < internalIdAllocators.size(); i++) {
@@ -472,110 +478,164 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
);
BlockBehavior blockBehavior = createBlockBehavior(customBlock, MiscUtils.getAsMapList(ResourceConfigUtils.get(section, "behavior", "behaviors")));
- // 单状态
+ Map> appearanceConfigs;
+ Map> futureVisualStates = new HashMap<>();
if (singleState) {
- BlockStateWrapper appearanceState = parsePluginFormattedBlockState(ResourceConfigUtils.requireNonEmptyStringOrThrow(stateSection.get("state"), "warning.config.block.state.missing_state"));
- this.arrangeModelForStateAndVerify(appearanceState, ResourceConfigUtils.get(stateSection, "model", "models"));
- ImmutableBlockState onlyState = states.getFirst();
- // 为唯一的状态绑定外观
- onlyState.setVanillaBlockState(appearanceState);
- parseBlockEntityRender(stateSection.get("entity-renderer")).ifPresent(onlyState::setConstantRenderers);
+ appearanceConfigs = Map.of("", stateSection);
} else {
- BlockStateWrapper anyAppearanceState = null;
- Map appearancesSection = ResourceConfigUtils.getAsMap(ResourceConfigUtils.requireNonNullOrThrow(stateSection.get("appearances"), "warning.config.block.state.missing_appearances"), "appearances");
- // 也不能为空
- if (appearancesSection.isEmpty()) {
- throw new LocalizedResourceConfigException("warning.config.block.state.missing_appearances");
- }
- Map appearances = Maps.newHashMap();
- // 先解析所有的外观
- for (Map.Entry entry : appearancesSection.entrySet()) {
- Map appearanceSection = ResourceConfigUtils.getAsMap(entry.getValue(), entry.getKey());
- // 解析对应的视觉方块
- BlockStateWrapper appearanceState = parsePluginFormattedBlockState(ResourceConfigUtils.requireNonEmptyStringOrThrow(appearanceSection.get("state"), "warning.config.block.state.missing_state"));
- this.arrangeModelForStateAndVerify(appearanceState, ResourceConfigUtils.get(appearanceSection, "model", "models"));
- appearances.put(entry.getKey(), new BlockStateAppearance(appearanceState, parseBlockEntityRender(appearanceSection.get("entity-renderer"))));
- if (anyAppearanceState == null) {
- anyAppearanceState = appearanceState;
- }
- }
- // 解析变体
- Map variantsSection = ResourceConfigUtils.getAsMap(ResourceConfigUtils.requireNonNullOrThrow(stateSection.get("variants"), "warning.config.block.state.missing_variants"), "variants");
- for (Map.Entry entry : variantsSection.entrySet()) {
- Map variantSection = ResourceConfigUtils.getAsMap(entry.getValue(), entry.getKey());
- String variantNBT = entry.getKey();
- // 先解析nbt,找到需要修改的方块状态
- CompoundTag tag = BlockNbtParser.deserialize(variantProvider, variantNBT);
- if (tag == null) {
- throw new LocalizedResourceConfigException("warning.config.block.state.property.invalid_format", variantNBT);
- }
- List possibleStates = variantProvider.getPossibleStates(tag);
- Map anotherSetting = ResourceConfigUtils.getAsMapOrNull(variantSection.get("settings"), "settings");
- if (anotherSetting != null) {
- for (ImmutableBlockState possibleState : possibleStates) {
- possibleState.setSettings(BlockSettings.ofFullCopy(possibleState.settings(), anotherSetting));
- }
- }
- String appearanceName = ResourceConfigUtils.getAsString(variantSection.get("appearance"));
- if (appearanceName != null) {
- BlockStateAppearance appearance = appearances.get(appearanceName);
- if (appearance == null) {
- throw new LocalizedResourceConfigException("warning.config.block.state.variant.invalid_appearance", variantNBT, appearanceName);
- }
- for (ImmutableBlockState possibleState : possibleStates) {
- possibleState.setVanillaBlockState(appearance.blockState());
- appearance.blockEntityRenderer().ifPresent(possibleState::setConstantRenderers);
- }
- }
- }
- // 为没有外观的方块状态填充
- for (ImmutableBlockState blockState : states) {
- if (blockState.vanillaBlockState() == null) {
- blockState.setVanillaBlockState(anyAppearanceState);
- }
+ Map rawAppearancesSection = ResourceConfigUtils.getAsMap(ResourceConfigUtils.requireNonNullOrThrow(stateSection.get("appearances"), "warning.config.block.state.missing_appearances"), "appearances");
+ appearanceConfigs = new LinkedHashMap<>(4);
+ for (Map.Entry entry : rawAppearancesSection.entrySet()) {
+ appearanceConfigs.put(entry.getKey(), ResourceConfigUtils.getAsMap(entry.getValue(), entry.getKey()));
}
}
- // 获取方块实体行为
- EntityBlockBehavior entityBlockBehavior = blockBehavior.getEntityBehavior();
- boolean isEntityBlock = entityBlockBehavior != null;
-
- // 绑定行为
- for (ImmutableBlockState state : states) {
- if (isEntityBlock) {
- state.setBlockEntityType(entityBlockBehavior.blockEntityType());
- }
- state.setBehavior(blockBehavior);
- int internalId = state.customBlockState().registryId();
- BlockStateWrapper visualState = state.vanillaBlockState();
- int appearanceId = visualState.registryId();
- int index = internalId - AbstractBlockManager.this.vanillaBlockStateCount;
- AbstractBlockManager.this.immutableBlockStates[index] = state;
- AbstractBlockManager.this.blockStateMappings[internalId] = appearanceId;
- AbstractBlockManager.this.appearanceToRealState.computeIfAbsent(appearanceId, k -> new IntArrayList()).add(internalId);
- AbstractBlockManager.this.tempVisualBlockStatesInUse.add(visualState);
- AbstractBlockManager.this.tempVisualBlocksInUse.add(getBlockOwnerId(visualState));
- AbstractBlockManager.this.applyPlatformSettings(state);
- // generate mod assets
- if (Config.generateModAssets()) {
- AbstractBlockManager.this.modBlockStateOverrides.put(BlockManager.createCustomBlockKey(index), Optional.ofNullable(AbstractBlockManager.this.tempVanillaBlockStateModels.get(appearanceId))
- .orElseGet(() -> {
- // 如果未指定模型,说明复用原版模型?但是部分模型是多部位模型,无法使用变体解决问题
- // 未来需要靠mod重构彻底解决问题
- JsonObject json = new JsonObject();
- json.addProperty("model", "minecraft:block/air");
- return json;
- }));
+ for (Map.Entry> entry : appearanceConfigs.entrySet()) {
+ Map appearanceSection = entry.getValue();
+ if (appearanceSection.containsKey("state")) {
+ String appearanceName = entry.getKey();
+ futureVisualStates.put(
+ appearanceName,
+ this.visualBlockStateAllocator.assignFixedBlockState(appearanceName.isEmpty() ? id.asString() : id.asString() + ":" + appearanceName, parsePluginFormattedBlockState(appearanceSection.get("state").toString()))
+ );
+ } else if (stateSection.containsKey("auto-state")) {
+ String autoStateId = stateSection.get("auto-state").toString();
+ AutoStateGroup group = AutoStateGroup.byId(autoStateId);
+ if (group == null) {
+ throw new LocalizedResourceConfigException("warning.config.block.state.invalid_auto_state", autoStateId, EnumUtils.toString(AutoStateGroup.values()));
+ }
+ String appearanceName = entry.getKey();
+ futureVisualStates.put(
+ appearanceName,
+ this.visualBlockStateAllocator.requestAutoState(appearanceName.isEmpty() ? id.asString() : id.asString() + ":" + appearanceName, group)
+ );
+ } else {
+ throw new LocalizedResourceConfigException("warning.config.block.state.missing_state");
}
}
- // 一定要到最后再绑定
- customBlock.setBehavior(blockBehavior);
- holder.bindValue(customBlock);
+ CompletableFutures.allOf(futureVisualStates.values()).whenComplete((v2, t2) -> {
+ if (t2 != null) {
+ if (t2 instanceof CompletionException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof VisualBlockStateAllocator.StateExhaustedException exhausted) {
+ throw new LocalizedResourceConfigException("warning.config.block.state.auto_state.exhausted", exhausted.group().id(), String.valueOf(exhausted.group().candidateCount()));
+ } else {
+ Debugger.BLOCK.warn(() -> "Unknown error while allocating visual block state.", cause);
+ return;
+ }
+ }
+ throw new RuntimeException("Unknown error occurred", t2);
+ }
- // 添加方块
- AbstractBlockManager.this.byId.put(customBlock.id(), customBlock);
+ BlockStateAppearance anyAppearance = null;
+ Map appearances = new HashMap<>();
+ for (Map.Entry> entry : appearanceConfigs.entrySet()) {
+ String appearanceName = entry.getKey();
+ Map appearanceSection = entry.getValue();
+ BlockStateWrapper visualBlockState;
+ try {
+ visualBlockState = futureVisualStates.get(appearanceName).get();
+ } catch (InterruptedException | ExecutionException e) {
+ AbstractBlockManager.this.plugin.logger().warn("Interrupted while allocating visual block state for block " + id.asString(), e);
+ return;
+ }
+ this.arrangeModelForStateAndVerify(visualBlockState, ResourceConfigUtils.get(appearanceSection, "model", "models"));
+ BlockStateAppearance blockStateAppearance = new BlockStateAppearance(visualBlockState, parseBlockEntityRender(appearanceSection.get("entity-renderer")));
+ appearances.put(appearanceName, blockStateAppearance);
+ if (anyAppearance == null) {
+ anyAppearance = blockStateAppearance;
+ }
+ }
+ // 至少有一个外观吧
+ Objects.requireNonNull(anyAppearance, "any appearance should not be null");
+
+ ExceptionCollector exceptionCollector = new ExceptionCollector<>();
+ if (!singleState) {
+ Map variantsSection = ResourceConfigUtils.getAsMapOrNull(stateSection.get("variants"), "variants");
+ if (variantsSection != null) {
+ for (Map.Entry entry : variantsSection.entrySet()) {
+ Map variantSection = ResourceConfigUtils.getAsMap(entry.getValue(), entry.getKey());
+ String variantNBT = entry.getKey();
+ // 先解析nbt,找到需要修改的方块状态
+ CompoundTag tag = BlockNbtParser.deserialize(variantProvider, variantNBT);
+ if (tag == null) {
+ exceptionCollector.add(new LocalizedResourceConfigException("warning.config.block.state.property.invalid_format", variantNBT));
+ continue;
+ }
+ List possibleStates = variantProvider.getPossibleStates(tag);
+ Map anotherSetting = ResourceConfigUtils.getAsMapOrNull(variantSection.get("settings"), "settings");
+ if (anotherSetting != null) {
+ for (ImmutableBlockState possibleState : possibleStates) {
+ possibleState.setSettings(BlockSettings.ofFullCopy(possibleState.settings(), anotherSetting));
+ }
+ }
+ String appearanceName = ResourceConfigUtils.getAsString(variantSection.get("appearance"));
+ if (appearanceName != null) {
+ BlockStateAppearance appearance = appearances.get(appearanceName);
+ if (appearance == null) {
+ exceptionCollector.add(new LocalizedResourceConfigException("warning.config.block.state.variant.invalid_appearance", variantNBT, appearanceName));
+ continue;
+ }
+ for (ImmutableBlockState possibleState : possibleStates) {
+ possibleState.setVanillaBlockState(appearance.blockState());
+ appearance.blockEntityRenderer().ifPresent(possibleState::setConstantRenderers);
+ }
+ }
+ }
+ }
+ }
+
+ // 获取方块实体行为
+ EntityBlockBehavior entityBlockBehavior = blockBehavior.getEntityBehavior();
+ boolean isEntityBlock = entityBlockBehavior != null;
+
+ // 绑定行为
+ for (ImmutableBlockState state : states) {
+ if (isEntityBlock) {
+ state.setBlockEntityType(entityBlockBehavior.blockEntityType());
+ }
+ state.setBehavior(blockBehavior);
+ int internalId = state.customBlockState().registryId();
+ BlockStateWrapper visualState = state.vanillaBlockState();
+ // 校验,为未绑定外观的强行添加外观
+ if (visualState == null) {
+ visualState = anyAppearance.blockState();
+ state.setVanillaBlockState(visualState);
+ anyAppearance.blockEntityRenderer().ifPresent(state::setConstantRenderers);
+ }
+ int appearanceId = visualState.registryId();
+ int index = internalId - AbstractBlockManager.this.vanillaBlockStateCount;
+ AbstractBlockManager.this.immutableBlockStates[index] = state;
+ AbstractBlockManager.this.blockStateMappings[internalId] = appearanceId;
+ AbstractBlockManager.this.appearanceToRealState.computeIfAbsent(appearanceId, k -> new IntArrayList()).add(internalId);
+ AbstractBlockManager.this.tempVisualBlockStatesInUse.add(visualState);
+ AbstractBlockManager.this.tempVisualBlocksInUse.add(getBlockOwnerId(visualState));
+ AbstractBlockManager.this.applyPlatformSettings(state);
+ // generate mod assets
+ if (Config.generateModAssets()) {
+ AbstractBlockManager.this.modBlockStateOverrides.put(BlockManager.createCustomBlockKey(index), Optional.ofNullable(AbstractBlockManager.this.tempVanillaBlockStateModels[appearanceId])
+ .orElseGet(() -> {
+ // 如果未指定模型,说明复用原版模型?但是部分模型是多部位模型,无法使用变体解决问题
+ // 未来需要靠mod重构彻底解决问题
+ JsonObject json = new JsonObject();
+ json.addProperty("model", "minecraft:block/air");
+ return json;
+ }));
+ }
+ }
+
+ // 一定要到最后再绑定
+ customBlock.setBehavior(blockBehavior);
+ holder.bindValue(customBlock);
+
+ // 添加方块
+ AbstractBlockManager.this.byId.put(customBlock.id(), customBlock);
+
+ // 抛出次要警告
+ exceptionCollector.throwIfPresent();
+ });
}, () -> GsonHelper.get().toJson(section)));
}
@@ -622,12 +682,12 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
// 结合variants
JsonElement combinedVariant = GsonHelper.combine(variants);
Map overrideMap = AbstractBlockManager.this.blockStateOverrides.computeIfAbsent(blockId, k -> new HashMap<>());
- AbstractBlockManager.this.tempVanillaBlockStateModels.put(blockStateWrapper.registryId(), combinedVariant);
JsonElement previous = overrideMap.get(propertyNBT);
if (previous != null && !previous.equals(combinedVariant)) {
throw new LocalizedResourceConfigException("warning.config.block.state.model.conflict", GsonHelper.get().toJson(combinedVariant), blockState, GsonHelper.get().toJson(previous));
}
overrideMap.put(propertyNBT, combinedVariant);
+ AbstractBlockManager.this.tempVanillaBlockStateModels[blockStateWrapper.registryId()] = combinedVariant;
}
private JsonObject parseAppearanceModelSectionAsJson(Map section) {
diff --git a/core/src/main/java/net/momirealms/craftengine/core/block/AbstractBlockStateWrapper.java b/core/src/main/java/net/momirealms/craftengine/core/block/AbstractBlockStateWrapper.java
index 7ce0ad545..4589397a0 100644
--- a/core/src/main/java/net/momirealms/craftengine/core/block/AbstractBlockStateWrapper.java
+++ b/core/src/main/java/net/momirealms/craftengine/core/block/AbstractBlockStateWrapper.java
@@ -23,4 +23,16 @@ public abstract class AbstractBlockStateWrapper implements BlockStateWrapper {
public String toString() {
return this.blockState.toString();
}
+
+ @Override
+ public final boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof BlockStateWrapper that)) return false;
+ return this.registryId == that.registryId();
+ }
+
+ @Override
+ public int hashCode() {
+ return this.registryId;
+ }
}
diff --git a/core/src/main/java/net/momirealms/craftengine/core/block/AutoStateGroup.java b/core/src/main/java/net/momirealms/craftengine/core/block/AutoStateGroup.java
index b79a46f55..f917f72b4 100644
--- a/core/src/main/java/net/momirealms/craftengine/core/block/AutoStateGroup.java
+++ b/core/src/main/java/net/momirealms/craftengine/core/block/AutoStateGroup.java
@@ -1,6 +1,8 @@
package net.momirealms.craftengine.core.block;
+import net.momirealms.craftengine.core.pack.allocator.BlockStateCandidate;
import net.momirealms.craftengine.core.util.Key;
+import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
@@ -9,37 +11,68 @@ import java.util.function.Predicate;
public enum AutoStateGroup {
LEAVES("leaves",
Set.of(BlockKeys.OAK_LEAVES, BlockKeys.SPRUCE_LEAVES, BlockKeys.BIRCH_LEAVES, BlockKeys.JUNGLE_LEAVES, BlockKeys.ACACIA_LEAVES, BlockKeys.DARK_OAK_LEAVES, BlockKeys.MANGROVE_LEAVES, BlockKeys.CHERRY_LEAVES, BlockKeys.PALE_OAK_LEAVES, BlockKeys.AZALEA_LEAVES, BlockKeys.FLOWERING_AZALEA_LEAVES),
- (w) -> !(boolean) w.getProperty("waterlogged"), 0
+ (w) -> !(boolean) w.getProperty("waterlogged")
),
WATERLOGGED_LEAVES(
"waterlogged_leaves",
Set.of(BlockKeys.OAK_LEAVES, BlockKeys.SPRUCE_LEAVES, BlockKeys.BIRCH_LEAVES, BlockKeys.JUNGLE_LEAVES, BlockKeys.ACACIA_LEAVES, BlockKeys.DARK_OAK_LEAVES, BlockKeys.MANGROVE_LEAVES, BlockKeys.CHERRY_LEAVES, BlockKeys.PALE_OAK_LEAVES, BlockKeys.AZALEA_LEAVES, BlockKeys.FLOWERING_AZALEA_LEAVES),
- (w) -> w.getProperty("waterlogged"), 0
+ (w) -> w.getProperty("waterlogged")
),
- TRIPWIRE("tripwire", Set.of(BlockKeys.TRIPWIRE), (w) -> true, 1),
- LOWER_TRIPWIRE("lower_tripwire", Set.of(BlockKeys.TRIPWIRE), (w) -> w.getProperty("attached"), 0),
- HIGHER_TRIPWIRE("higher_tripwire", Set.of(BlockKeys.TRIPWIRE), (w) -> !(boolean) w.getProperty("attached"), 0),
- NOTE_BLOCK("note_block", Set.of(BlockKeys.NOTE_BLOCK), (w) -> true, 0),
- BROWN_MUSHROOM("brown_mushroom", Set.of(BlockKeys.BROWN_MUSHROOM_BLOCK), (w) -> true, 0),
- RED_MUSHROOM("red_mushroom", Set.of(BlockKeys.RED_MUSHROOM_BLOCK), (w) -> true, 0),
- MUSHROOM_STEM("mushroom_stem", Set.of(BlockKeys.MUSHROOM_STEM), (w) -> true, 0),
- MUSHROOM("mushroom", Set.of(BlockKeys.BROWN_MUSHROOM_BLOCK, BlockKeys.RED_MUSHROOM_BLOCK, BlockKeys.MUSHROOM_STEM), (w) -> true, 1),
- SOLID("solid", Set.of(BlockKeys.BROWN_MUSHROOM_BLOCK, BlockKeys.RED_MUSHROOM_BLOCK, BlockKeys.MUSHROOM_STEM, BlockKeys.NOTE_BLOCK), (w) -> true, 2);
+ LOWER_TRIPWIRE("lower_tripwire", Set.of(BlockKeys.TRIPWIRE), (w) -> w.getProperty("attached")),
+ HIGHER_TRIPWIRE("higher_tripwire", Set.of(BlockKeys.TRIPWIRE), (w) -> !(boolean) w.getProperty("attached")),
+ NOTE_BLOCK("note_block", Set.of(BlockKeys.NOTE_BLOCK), (w) -> true),
+ BROWN_MUSHROOM("brown_mushroom", Set.of(BlockKeys.BROWN_MUSHROOM_BLOCK), (w) -> true),
+ RED_MUSHROOM("red_mushroom", Set.of(BlockKeys.RED_MUSHROOM_BLOCK), (w) -> true),
+ MUSHROOM_STEM("mushroom_stem", Set.of(BlockKeys.MUSHROOM_STEM), (w) -> true),
+ TRIPWIRE("tripwire", Set.of(BlockKeys.TRIPWIRE), (w) -> true),
+ SUGAR_CANE("sugar_cane", Set.of(BlockKeys.SUGAR_CANE), (w) -> true),
+ CACTUS("cactus", Set.of(BlockKeys.CACTUS), (w) -> true),
+ SAPLING("sapling", Set.of(BlockKeys.OAK_SAPLING, BlockKeys.SPRUCE_SAPLING, BlockKeys.BIRCH_SAPLING, BlockKeys.JUNGLE_SAPLING, BlockKeys.ACACIA_SAPLING, BlockKeys.DARK_OAK_SAPLING, BlockKeys.CHERRY_SAPLING, BlockKeys.PALE_OAK_SAPLING), (w) -> true),
+ MUSHROOM("mushroom", Set.of(BlockKeys.BROWN_MUSHROOM_BLOCK, BlockKeys.RED_MUSHROOM_BLOCK, BlockKeys.MUSHROOM_STEM), (w) -> true),
+ SOLID("solid", Set.of(BlockKeys.BROWN_MUSHROOM_BLOCK, BlockKeys.RED_MUSHROOM_BLOCK, BlockKeys.MUSHROOM_STEM, BlockKeys.NOTE_BLOCK), (w) -> true);
private final Set blocks;
private final String id;
private final Predicate predicate;
- private final int priority;
+ private final List candidates = new ArrayList<>();
+ private int pointer;
- AutoStateGroup(String id, Set blocks, Predicate predicate, int priority) {
+ AutoStateGroup(String id, Set blocks, Predicate predicate) {
this.id = id;
this.blocks = blocks;
this.predicate = predicate;
- this.priority = priority;
}
- public int priority() {
- return priority;
+ public void reset() {
+ this.pointer = 0;
+ this.candidates.clear();
+ }
+
+ public void addCandidate(@NotNull BlockStateCandidate candidate) {
+ this.candidates.add(candidate);
+ }
+
+ public int candidateCount() {
+ return candidates.size();
+ }
+
+ @Nullable
+ public BlockStateCandidate findNextCandidate() {
+ while (this.pointer < this.candidates.size()) {
+ final BlockStateCandidate state = this.candidates.get(this.pointer);
+ if (!state.isUsed()) {
+ return state;
+ }
+ this.pointer++;
+ }
+ return null;
+ }
+
+ public boolean test(BlockStateWrapper state) {
+ if (!this.blocks.contains(state.ownerId())) {
+ return false;
+ }
+ return this.predicate.test(state);
}
public Set blocks() {
diff --git a/core/src/main/java/net/momirealms/craftengine/core/block/BlockKeys.java b/core/src/main/java/net/momirealms/craftengine/core/block/BlockKeys.java
index 30490bbad..94bbf2342 100644
--- a/core/src/main/java/net/momirealms/craftengine/core/block/BlockKeys.java
+++ b/core/src/main/java/net/momirealms/craftengine/core/block/BlockKeys.java
@@ -7,6 +7,7 @@ import java.util.List;
public final class BlockKeys {
private BlockKeys() {}
+ public static final Key SUGAR_CANE = Key.of("minecraft:sugar_cane");
public static final Key NOTE_BLOCK = Key.of("minecraft:note_block");
public static final Key TRIPWIRE = Key.of("minecraft:tripwire");
public static final Key CRAFTING_TABLE = Key.of("minecraft:crafting_table");
@@ -262,6 +263,15 @@ public final class BlockKeys {
public static final Key AZALEA_LEAVES = Key.of("minecraft:azalea_leaves");
public static final Key FLOWERING_AZALEA_LEAVES = Key.of("minecraft:flowering_azalea_leaves");
+ public static final Key OAK_SAPLING = Key.of("minecraft:oak_sapling");
+ public static final Key SPRUCE_SAPLING = Key.of("minecraft:spruce_sapling");
+ public static final Key BIRCH_SAPLING = Key.of("minecraft:birch_sapling");
+ public static final Key JUNGLE_SAPLING = Key.of("minecraft:jungle_sapling");
+ public static final Key DARK_OAK_SAPLING = Key.of("minecraft:dark_oak_sapling");
+ public static final Key ACACIA_SAPLING = Key.of("minecraft:acacia_sapling");
+ public static final Key CHERRY_SAPLING = Key.of("minecraft:cherry_sapling");
+ public static final Key PALE_OAK_SAPLING = Key.of("minecraft:pale_oak_sapling");
+
public static final List WOODEN_TRAPDOORS = List.of(OAK_TRAPDOOR, SPRUCE_TRAPDOOR, BIRCH_TRAPDOOR,
ACACIA_TRAPDOOR, PALE_OAK_TRAPDOOR, DARK_OAK_TRAPDOOR, MANGROVE_TRAPDOOR, JUNGLE_TRAPDOOR);
public static final List CHERRY_TRAPDOORS = List.of(CHERRY_TRAPDOOR);
diff --git a/core/src/main/java/net/momirealms/craftengine/core/block/BlockStateWrapper.java b/core/src/main/java/net/momirealms/craftengine/core/block/BlockStateWrapper.java
index 0b19c2ee3..5ef5223b1 100644
--- a/core/src/main/java/net/momirealms/craftengine/core/block/BlockStateWrapper.java
+++ b/core/src/main/java/net/momirealms/craftengine/core/block/BlockStateWrapper.java
@@ -1,8 +1,9 @@
package net.momirealms.craftengine.core.block;
import net.momirealms.craftengine.core.util.Key;
+import org.jetbrains.annotations.NotNull;
-public interface BlockStateWrapper {
+public interface BlockStateWrapper extends Comparable {
Object literalObject();
@@ -15,4 +16,9 @@ public interface BlockStateWrapper {
boolean hasProperty(String propertyName);
String getAsString();
+
+ @Override
+ default int compareTo(@NotNull BlockStateWrapper o) {
+ return Integer.compare(registryId(), o.registryId());
+ }
}
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 f8ed7e7c3..5c44b4428 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
@@ -80,6 +80,16 @@ public abstract class AbstractFontManager implements FontManager {
return offsetFont;
}
+ @Override
+ public Map loadedImages() {
+ return Collections.unmodifiableMap(this.images);
+ }
+
+ @Override
+ public Map emojis() {
+ return Collections.unmodifiableMap(this.emojis);
+ }
+
@Override
public void unload() {
this.fonts.clear();
diff --git a/core/src/main/java/net/momirealms/craftengine/core/font/FontManager.java b/core/src/main/java/net/momirealms/craftengine/core/font/FontManager.java
index e56e39c3b..7cf65807b 100644
--- a/core/src/main/java/net/momirealms/craftengine/core/font/FontManager.java
+++ b/core/src/main/java/net/momirealms/craftengine/core/font/FontManager.java
@@ -48,6 +48,10 @@ public interface FontManager extends Manageable {
OffsetFont offsetFont();
+ Map loadedImages();
+
+ Map emojis();
+
ConfigParser[] parsers();
default EmojiTextProcessResult replaceMiniMessageEmoji(@NotNull String miniMessage, @Nullable Player player) {
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 b1d0539ed..9d3c7b909 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
@@ -69,6 +69,14 @@ public abstract class AbstractItemManager extends AbstractModelGenerator impl
ItemDataModifiers.init();
}
+ public ItemParser itemParser() {
+ return itemParser;
+ }
+
+ public EquipmentParser equipmentParser() {
+ return equipmentParser;
+ }
+
protected static void registerVanillaItemExtraBehavior(ItemBehavior behavior, Key... items) {
for (Key key : items) {
VANILLA_ITEM_EXTRA_BEHAVIORS.computeIfAbsent(key, k -> new ArrayList<>()).add(behavior);
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 c4ed3e352..6ee669d9a 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
@@ -5,7 +5,6 @@ import com.google.common.collect.Multimap;
import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs;
import com.google.gson.*;
-import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.momirealms.craftengine.core.font.BitmapImage;
import net.momirealms.craftengine.core.font.Font;
import net.momirealms.craftengine.core.item.equipment.ComponentBasedEquipment;
@@ -27,7 +26,9 @@ import net.momirealms.craftengine.core.pack.obfuscation.ObfA;
import net.momirealms.craftengine.core.pack.revision.Revision;
import net.momirealms.craftengine.core.pack.revision.Revisions;
import net.momirealms.craftengine.core.plugin.CraftEngine;
-import net.momirealms.craftengine.core.plugin.config.*;
+import net.momirealms.craftengine.core.plugin.config.Config;
+import net.momirealms.craftengine.core.plugin.config.ConfigParser;
+import net.momirealms.craftengine.core.plugin.config.StringKeyConstructor;
import net.momirealms.craftengine.core.plugin.locale.I18NData;
import net.momirealms.craftengine.core.plugin.locale.LocalizedException;
import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException;
@@ -615,7 +616,10 @@ public abstract class AbstractPackManager implements PackManager {
long o2 = System.nanoTime();
this.plugin.logger().info("Loaded packs. Took " + String.format("%.2f", ((o2 - o1) / 1_000_000.0)) + " ms");
for (ConfigParser parser : this.sortedParsers) {
- if (!predicate.test(parser)) continue;
+ if (!predicate.test(parser)) {
+ parser.clear();
+ continue;
+ }
long t1 = System.nanoTime();
parser.preProcess();
parser.loadAll();
diff --git a/core/src/main/java/net/momirealms/craftengine/core/pack/allocator/BlockStateAllocator.java b/core/src/main/java/net/momirealms/craftengine/core/pack/allocator/BlockStateAllocator.java
deleted file mode 100644
index 5669d0b27..000000000
--- a/core/src/main/java/net/momirealms/craftengine/core/pack/allocator/BlockStateAllocator.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package net.momirealms.craftengine.core.pack.allocator;
-
-import org.jetbrains.annotations.Nullable;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class BlockStateAllocator {
- private final List blockStates = new ArrayList<>();
- private int pointer = 0;
- private int max = -1;
-
- public void addCandidate(BlockStateCandidate state) {
- this.blockStates.add(state);
- this.max = this.blockStates.size() - 1;
- }
-
- @Nullable
- public BlockStateCandidate findNext() {
- while (this.pointer < this.max) {
- final BlockStateCandidate state = this.blockStates.get(this.pointer);
- if (!state.isUsed()) {
- return state;
- }
- this.pointer++;
- }
- return null;
- }
-
- public void processPendingAllocations() {
-
- }
-}
diff --git a/core/src/main/java/net/momirealms/craftengine/core/pack/allocator/IdAllocator.java b/core/src/main/java/net/momirealms/craftengine/core/pack/allocator/IdAllocator.java
index 47c5e6bd3..9a84c875a 100644
--- a/core/src/main/java/net/momirealms/craftengine/core/pack/allocator/IdAllocator.java
+++ b/core/src/main/java/net/momirealms/craftengine/core/pack/allocator/IdAllocator.java
@@ -152,7 +152,7 @@ public class IdAllocator {
* @param shouldRemove 判断是否应该移除的谓词
* @return 被移除的ID数量
*/
- public int cleanupUnusedIds(Predicate shouldRemove) {
+ public List cleanupUnusedIds(Predicate shouldRemove) {
List idsToRemove = new ArrayList<>();
for (String id : this.cachedIdMap.keySet()) {
if (shouldRemove.test(id)) {
@@ -160,15 +160,13 @@ public class IdAllocator {
}
}
- 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;
+ return idsToRemove;
}
/**
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
new file mode 100644
index 000000000..ab7d7cd86
--- /dev/null
+++ b/core/src/main/java/net/momirealms/craftengine/core/pack/allocator/VisualBlockStateAllocator.java
@@ -0,0 +1,152 @@
+package net.momirealms.craftengine.core.pack.allocator;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import net.momirealms.craftengine.core.block.AutoStateGroup;
+import net.momirealms.craftengine.core.block.BlockStateWrapper;
+import net.momirealms.craftengine.core.util.FileUtils;
+import net.momirealms.craftengine.core.util.GsonHelper;
+import net.momirealms.craftengine.core.util.Pair;
+
+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.Function;
+
+public class VisualBlockStateAllocator {
+ private final Path cacheFilePath;
+ private final Map cachedBlockStates = new HashMap<>();
+ private final Map>> pendingAllocations = new LinkedHashMap<>();
+ @SuppressWarnings("unchecked")
+ private final List>>[] pendingAllocationFutures = new List[AutoStateGroup.values().length];
+ private final BlockStateCandidate[] candidates;
+ private final Function factory;
+
+ public VisualBlockStateAllocator(Path cacheFilePath, BlockStateCandidate[] candidates, Function factory) {
+ this.cacheFilePath = cacheFilePath;
+ this.candidates = candidates;
+ this.factory = factory;
+ }
+
+ public void reset() {
+ Arrays.fill(this.pendingAllocationFutures, new ArrayList<>());
+ this.cachedBlockStates.clear();
+ this.pendingAllocations.clear();
+ }
+
+ public CompletableFuture assignFixedBlockState(String name, BlockStateWrapper state) {
+ this.cachedBlockStates.remove(name);
+ BlockStateCandidate candidate = this.candidates[state.registryId()];
+ if (candidate != null) {
+ candidate.setUsed();
+ }
+ return CompletableFuture.completedFuture(state);
+ }
+
+ public CompletableFuture requestAutoState(String name, AutoStateGroup group) {
+ CompletableFuture future = new CompletableFuture<>();
+ this.pendingAllocations.put(name, new Pair<>(group, future));
+ this.pendingAllocationFutures[group.ordinal()].add(Pair.of(name, future));
+ return future;
+ }
+
+ public void processPendingAllocations() {
+ // 先处理缓存的
+ for (Map.Entry entry : this.cachedBlockStates.entrySet()) {
+ int registryId = entry.getValue().registryId();
+ // 检查候选方块是否可用
+ BlockStateCandidate candidate = this.candidates[registryId];
+ if (candidate != null) {
+ // 未被使用
+ if (!candidate.isUsed()) {
+ // 获取当前的安排任务
+ Pair> pair = this.pendingAllocations.get(entry.getKey());
+ // 如果候选满足组,那么直接允许起飞
+ if (pair != null && pair.left().test(candidate.blockState())) {
+ pair.right().complete(candidate.blockState());
+ }
+ // 尽管未被使用,该槽位也应该被占用,以避免被自动分配到
+ candidate.setUsed();
+ }
+ // 被使用了就随他去
+ }
+ // 没有候选也随他去
+ }
+
+ this.pendingAllocations.clear();
+
+ for (AutoStateGroup group : AutoStateGroup.values()) {
+ List>> pendingAllocationFuture = this.pendingAllocationFutures[group.ordinal()];
+ for (Pair> pair : pendingAllocationFuture) {
+ BlockStateCandidate nextCandidate = group.findNextCandidate();
+ if (nextCandidate != null) {
+ nextCandidate.setUsed();
+ this.cachedBlockStates.put(pair.left(), nextCandidate.blockState());
+ pair.right().complete(nextCandidate.blockState());
+ } else {
+ pair.right().completeExceptionally(new StateExhaustedException(group));
+ }
+ }
+ }
+ }
+
+ public static class StateExhaustedException extends RuntimeException {
+ private final AutoStateGroup group;
+
+ public StateExhaustedException(AutoStateGroup group) {
+ this.group = group;
+ }
+
+ public AutoStateGroup group() {
+ return group;
+ }
+ }
+
+ /**
+ * 从文件加载缓存
+ */
+ 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) {
+ String id = primitive.getAsString();
+ BlockStateWrapper state = this.factory.apply(id);
+ if (state != null) {
+ this.cachedBlockStates.put(entry.getKey(), state);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * 保存缓存到文件
+ */
+ public void saveToCache() throws IOException {
+ // 创建按ID排序的TreeMap
+ Map sortedById = new TreeMap<>();
+ for (Map.Entry entry : this.cachedBlockStates.entrySet()) {
+ sortedById.put(entry.getValue(), entry.getKey());
+ }
+ // 创建有序的JSON对象
+ JsonObject sortedJsonObject = new JsonObject();
+ for (Map.Entry entry : sortedById.entrySet()) {
+ sortedJsonObject.addProperty(entry.getValue(), entry.getKey().getAsString());
+ }
+ if (sortedJsonObject.isEmpty()) {
+ if (Files.exists(this.cacheFilePath)) {
+ Files.delete(this.cacheFilePath);
+ }
+ } else {
+ FileUtils.createDirectoriesSafe(this.cacheFilePath.getParent());
+ GsonHelper.writeJsonFile(sortedJsonObject, this.cacheFilePath);
+ }
+ }
+}
diff --git a/core/src/main/java/net/momirealms/craftengine/core/pack/allocator/cache/AllocationCacheFile.java b/core/src/main/java/net/momirealms/craftengine/core/pack/allocator/cache/AllocationCacheFile.java
new file mode 100644
index 000000000..4a636da69
--- /dev/null
+++ b/core/src/main/java/net/momirealms/craftengine/core/pack/allocator/cache/AllocationCacheFile.java
@@ -0,0 +1,62 @@
+package net.momirealms.craftengine.core.pack.allocator.cache;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.CompletableFuture;
+
+public class AllocationCacheFile, A> {
+ private final Map cache;
+ private final CacheStorage cacheStorage;
+ private final CacheSerializer serializer;
+
+ public AllocationCacheFile(CacheStorage cacheStorage, CacheSerializer serializer) {
+ this.cache = new HashMap<>();
+ this.cacheStorage = cacheStorage;
+ this.serializer = serializer;
+ }
+
+ public Map cache() {
+ return this.cache;
+ }
+
+ public void clear() {
+ this.cache.clear();
+ }
+
+ public CompletableFuture load() {
+ return this.cacheStorage.load().thenAccept(a -> {
+ Map deserialized = this.serializer.deserialize(a);
+ this.cache.putAll(deserialized);
+ });
+ }
+
+ public CompletableFuture save() {
+ Map sortedById = new TreeMap<>();
+ for (Map.Entry entry : this.cache.entrySet()) {
+ sortedById.put(entry.getValue(), entry.getKey());
+ }
+ return this.cacheStorage.save(this.serializer.serialize(sortedById));
+ }
+
+ public Iterable> entrySet() {
+ return this.cache.entrySet();
+ }
+
+ public Set keySet() {
+ return this.cache.keySet();
+ }
+
+ public void put(String name, T newId) {
+ this.cache.put(name, newId);
+ }
+
+ public T remove(String name) {
+ return this.cache.remove(name);
+ }
+
+ public T get(String name) {
+ return this.cache.get(name);
+ }
+}
diff --git a/core/src/main/java/net/momirealms/craftengine/core/pack/allocator/cache/CacheFileStorage.java b/core/src/main/java/net/momirealms/craftengine/core/pack/allocator/cache/CacheFileStorage.java
new file mode 100644
index 000000000..d230558fa
--- /dev/null
+++ b/core/src/main/java/net/momirealms/craftengine/core/pack/allocator/cache/CacheFileStorage.java
@@ -0,0 +1,98 @@
+package net.momirealms.craftengine.core.pack.allocator.cache;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.CompletableFuture;
+
+public interface CacheFileStorage {
+
+ CompletableFuture load();
+
+ CompletableFuture save(A value);
+
+ boolean needForceUpdate();
+
+ static LocalFileCacheStorage local(Path path, CacheFileType type) {
+ return new LocalFileCacheStorage<>(path, type);
+ }
+
+ abstract class AbstractRemoteFileCacheStorage implements CacheFileStorage {
+
+ @Override
+ public boolean needForceUpdate() {
+ return true;
+ }
+ }
+
+ class LocalFileCacheStorage implements CacheFileStorage {
+ private final CacheFileType fileType;
+ private final Path filePath;
+ private long lastModified = 0L;
+
+ public LocalFileCacheStorage(Path filePath, CacheFileType type) {
+ this.filePath = filePath;
+ this.fileType = type;
+ updateLastModified();
+ }
+
+ @Override
+ public boolean needForceUpdate() {
+ try {
+ if (!Files.exists(this.filePath)) {
+ return this.lastModified != 0L; // 文件被删除了,需要更新
+ }
+ long currentModified = Files.getLastModifiedTime(this.filePath).toMillis();
+ if (currentModified > this.lastModified) {
+ this.lastModified = currentModified;
+ return true; // 文件被修改了,需要强制更新
+ }
+ return false;
+ } catch (IOException e) {
+ // 如果无法读取文件信息,保守起见返回 true 强制更新
+ return true;
+ }
+ }
+
+ @Override
+ public CompletableFuture load() {
+ if (!Files.exists(this.filePath)) {
+ this.lastModified = 0L; // 重置最后修改时间
+ return CompletableFuture.completedFuture(this.fileType.create());
+ }
+ try {
+ A result = this.fileType.read(this.filePath);
+ updateLastModified(); // 加载成功后更新最后修改时间
+ return CompletableFuture.completedFuture(result);
+ } catch (Exception e) {
+ return CompletableFuture.failedFuture(e);
+ }
+ }
+
+ @Override
+ public CompletableFuture save(A value) {
+ try {
+ this.fileType.write(this.filePath, value);
+ updateLastModified(); // 保存成功后更新最后修改时间
+ return CompletableFuture.completedFuture(null);
+ } catch (Exception e) {
+ return CompletableFuture.failedFuture(e);
+ }
+ }
+
+ /**
+ * 更新最后修改时间
+ */
+ private void updateLastModified() {
+ try {
+ if (Files.exists(this.filePath)) {
+ this.lastModified = Files.getLastModifiedTime(filePath).toMillis();
+ } else {
+ this.lastModified = 0L;
+ }
+ } catch (IOException e) {
+ this.lastModified = 0L; // 出错时重置
+ }
+ }
+ }
+}
diff --git a/core/src/main/java/net/momirealms/craftengine/core/pack/allocator/cache/CacheFileType.java b/core/src/main/java/net/momirealms/craftengine/core/pack/allocator/cache/CacheFileType.java
new file mode 100644
index 000000000..b4446a511
--- /dev/null
+++ b/core/src/main/java/net/momirealms/craftengine/core/pack/allocator/cache/CacheFileType.java
@@ -0,0 +1,39 @@
+package net.momirealms.craftengine.core.pack.allocator.cache;
+
+import com.google.gson.JsonObject;
+import net.momirealms.craftengine.core.util.GsonHelper;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public interface CacheFileType {
+ JsonCacheFileType JSON = new JsonCacheFileType();
+
+ T read(Path path) throws IOException;
+
+ void write(Path path, T value) throws IOException;
+
+ T create();
+
+ class JsonCacheFileType implements CacheFileType {
+
+ @Override
+ public JsonObject read(Path path) throws IOException {
+ if (Files.exists(path)) {
+ return GsonHelper.readJsonFile(path).getAsJsonObject();
+ }
+ return new JsonObject();
+ }
+
+ @Override
+ public void write(Path path, JsonObject value) throws IOException {
+ GsonHelper.writeJsonFile(value, path);
+ }
+
+ @Override
+ public JsonObject create() {
+ return new JsonObject();
+ }
+ }
+}
diff --git a/core/src/main/java/net/momirealms/craftengine/core/pack/allocator/cache/CacheSerializer.java b/core/src/main/java/net/momirealms/craftengine/core/pack/allocator/cache/CacheSerializer.java
new file mode 100644
index 000000000..161caad29
--- /dev/null
+++ b/core/src/main/java/net/momirealms/craftengine/core/pack/allocator/cache/CacheSerializer.java
@@ -0,0 +1,51 @@
+package net.momirealms.craftengine.core.pack.allocator.cache;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public interface CacheSerializer, A> {
+
+ A serialize(Map obj);
+
+ Map deserialize(A obj);
+
+ static > CacheSerializer json() {
+ return new JsonSerializer<>();
+ }
+
+ class JsonSerializer> implements CacheSerializer {
+
+ @Override
+ public JsonObject serialize(Map obj) {
+ JsonObject jsonObject = new JsonObject();
+ for (Map.Entry entry : obj.entrySet()) {
+ if (entry.getKey() instanceof Integer i) {
+ jsonObject.addProperty(entry.getValue(), i);
+ } else if (entry.getKey() instanceof String s) {
+ jsonObject.addProperty(entry.getValue(), s);
+ }
+ }
+ return jsonObject;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Map deserialize(JsonObject obj) {
+ Map map = new HashMap<>();
+ for (Map.Entry entry : obj.entrySet()) {
+ if (entry.getValue() instanceof JsonPrimitive primitive) {
+ if (primitive.isNumber()) {
+ map.put(entry.getKey(), (T) (Integer) primitive.getAsInt());
+ } else if (primitive.isString()) {
+ map.put(entry.getKey(), (T) primitive.getAsString());
+ }
+ }
+ }
+ return map;
+ }
+ }
+}
diff --git a/core/src/main/java/net/momirealms/craftengine/core/pack/allocator/cache/CacheStorage.java b/core/src/main/java/net/momirealms/craftengine/core/pack/allocator/cache/CacheStorage.java
new file mode 100644
index 000000000..10856d762
--- /dev/null
+++ b/core/src/main/java/net/momirealms/craftengine/core/pack/allocator/cache/CacheStorage.java
@@ -0,0 +1,32 @@
+package net.momirealms.craftengine.core.pack.allocator.cache;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.concurrent.CompletableFuture;
+
+public class CacheStorage {
+ private final CacheFileStorage storage;
+ private A lastReadValue;
+
+ public CacheStorage(CacheFileStorage storage) {
+ this.storage = storage;
+ }
+
+ public CompletableFuture save(@NotNull final A value) {
+ if (!value.equals(this.lastReadValue) || this.storage.needForceUpdate()) {
+ this.lastReadValue = value;
+ return this.storage.save(value);
+ }
+ return CompletableFuture.completedFuture(null);
+ }
+
+ public CompletableFuture load() {
+ if (this.lastReadValue != null && !this.storage.needForceUpdate()) {
+ return CompletableFuture.completedFuture(this.lastReadValue);
+ }
+ return this.storage.load().thenApply(a -> {
+ this.lastReadValue = a;
+ return a;
+ });
+ }
+}