diff --git a/bukkit/loader/src/main/resources/resources/default/configuration/fix_client_visual.yml b/bukkit/loader/src/main/resources/resources/default/configuration/fix_client_visual.yml index 795eae21a..8fc54c072 100644 --- a/bukkit/loader/src/main/resources/resources/default/configuration/fix_client_visual.yml +++ b/bukkit/loader/src/main/resources/resources/default/configuration/fix_client_visual.yml @@ -1,5 +1,5 @@ -# client-bound-data requires CraftEngine mod to apply items: + # client-bound-data requires CraftEngine mod to apply minecraft:string: client-bound-data: components: @@ -17,4 +17,10 @@ items: minecraft:block_state: instrument: "harp" powered: "false" - note: "0" \ No newline at end of file + note: "0" + +blocks: + minecraft:note_block: + settings: + client-bound-tags: + - minecraft:beacon_base_block \ No newline at end of file diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/BlockEventListener.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/BlockEventListener.java index a31e0023a..e3b9f07cd 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/BlockEventListener.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/BlockEventListener.java @@ -33,6 +33,7 @@ import org.bukkit.event.block.BlockPhysicsEvent; import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.entity.EntityExplodeEvent; +import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.world.GenericGameEvent; import org.bukkit.inventory.ItemStack; @@ -49,6 +50,14 @@ public class BlockEventListener implements Listener { this.enableNoteBlockCheck = enableNoteBlockCheck; } + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + Object packet = this.manager.cachedUpdateTagsPacket; + if (packet != null) { + this.plugin.networkManager().sendPacket(event.getPlayer(), packet); + } + } + @EventHandler(ignoreCancelled = true) public void onPlayerAttack(EntityDamageByEntityEvent event) { if (!VersionHelper.isOrAbove1_20_5()) { 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 af14fc4d1..5e8d9a78a 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 @@ -11,10 +11,7 @@ import net.momirealms.craftengine.bukkit.nms.FastNMS; import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine; import net.momirealms.craftengine.bukkit.plugin.injector.BukkitInjector; import net.momirealms.craftengine.bukkit.plugin.network.PacketConsumers; -import net.momirealms.craftengine.bukkit.util.BlockStateUtils; -import net.momirealms.craftengine.bukkit.util.KeyUtils; -import net.momirealms.craftengine.bukkit.util.Reflections; -import net.momirealms.craftengine.bukkit.util.RegistryUtils; +import net.momirealms.craftengine.bukkit.util.*; import net.momirealms.craftengine.core.block.*; import net.momirealms.craftengine.core.block.properties.Properties; import net.momirealms.craftengine.core.block.properties.Property; @@ -34,12 +31,15 @@ import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigExce import net.momirealms.craftengine.core.plugin.locale.TranslationManager; import net.momirealms.craftengine.core.registry.BuiltInRegistries; import net.momirealms.craftengine.core.registry.Holder; +import net.momirealms.craftengine.core.registry.Registries; import net.momirealms.craftengine.core.registry.WritableRegistry; import net.momirealms.craftengine.core.util.*; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.NamespacedKey; +import org.bukkit.Registry; import org.bukkit.block.data.BlockData; +import org.bukkit.entity.Player; import org.bukkit.event.HandlerList; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; @@ -49,6 +49,7 @@ import java.io.File; import java.lang.reflect.Field; import java.nio.file.Path; import java.util.*; +import java.util.stream.Collectors; public class BukkitBlockManager extends AbstractBlockManager { private static BukkitBlockManager instance; @@ -92,6 +93,10 @@ public class BukkitBlockManager extends AbstractBlockManager { private final BlockEventListener blockEventListener; private final FallingBlockRemoveListener fallingBlockRemoveListener; + private Map> clientBoundTags = Map.of(); + private Map> previousTags = Map.of(); + protected Object cachedUpdateTagsPacket; + public BukkitBlockManager(BukkitCraftEngine plugin) { super(plugin); this.plugin = plugin; @@ -146,6 +151,8 @@ public class BukkitBlockManager extends AbstractBlockManager { this.modBlockStates.clear(); if (EmptyBlock.STATE != null) Arrays.fill(this.stateId2ImmutableBlockStates, EmptyBlock.STATE); + this.previousTags = this.clientBoundTags; + this.clientBoundTags = new HashMap<>(); } @Override @@ -165,6 +172,26 @@ public class BukkitBlockManager extends AbstractBlockManager { initSuggestions(); resetPacketConsumers(); clearCache(); + resendTags(); + } + + private void resendTags() { + // if there's no change + if (this.clientBoundTags.equals(this.previousTags)) return; + List list = new ArrayList<>(); + for (Map.Entry> entry : this.clientBoundTags.entrySet()) { + list.add(new TagUtils.TagEntry(entry.getKey(), entry.getValue())); + } + Object packet = TagUtils.createUpdateTagsPacket(Map.of(Reflections.instance$Registries$BLOCK, list)); + for (Player player : Bukkit.getOnlinePlayers()) { + this.plugin.networkManager().sendPacket(player, packet); + } + // 如果空,那么新来的玩家就没必要收到更新包了 + if (list.isEmpty()) { + this.cachedUpdateTagsPacket = null; + } else { + this.cachedUpdateTagsPacket = packet; + } } private void clearCache() { @@ -333,10 +360,18 @@ public class BukkitBlockManager extends AbstractBlockManager { @Override public void parseSection(Pack pack, Path path, Key id, Map section) { - // check duplicated config - if (byId.containsKey(id)) { - throw new LocalizedResourceConfigException("warning.config.block.duplicate"); + if (id.namespace().equals("minecraft") && Registry.MATERIAL.get(KeyUtils.toNamespacedKey(id)) != null) { + parseVanillaBlock(pack, path, id, section); + } else { + // check duplicated config + if (byId.containsKey(id)) { + throw new LocalizedResourceConfigException("warning.config.block.duplicate"); + } + parseCustomBlock(pack, path, id, section); } + } + + private void parseCustomBlock(Pack pack, Path path, Key id, Map section) { // read block settings BlockSettings settings = BlockSettings.fromMap(MiscUtils.castToMap(section.getOrDefault("settings", Map.of()), false)); @@ -458,14 +493,14 @@ public class BukkitBlockManager extends AbstractBlockManager { Map behaviors = MiscUtils.castToMap(section.getOrDefault("behavior", Map.of()), false); CustomBlock block = BukkitCustomBlock.builder(id) - .appearances(appearances) - .variantMapper(variants) - .lootTable(lootTable) - .properties(properties) - .settings(settings) - .behavior(behaviors) - .events(events) - .build(); + .appearances(appearances) + .variantMapper(variants) + .lootTable(lootTable) + .properties(properties) + .settings(settings) + .behavior(behaviors) + .events(events) + .build(); // bind appearance and real state for (ImmutableBlockState state : block.variantProvider().states()) { @@ -479,7 +514,7 @@ public class BukkitBlockManager extends AbstractBlockManager { appearanceToRealState.computeIfAbsent(state.vanillaBlockState().registryId(), k -> new ArrayList<>()).add(state.customBlockState().registryId()); } - byId.put(id, block); + BukkitBlockManager.this.byId.put(id, block); // generate mod assets if (Config.generateModAssets()) { @@ -489,6 +524,23 @@ public class BukkitBlockManager extends AbstractBlockManager { } } } + + private void parseVanillaBlock(Pack pack, Path path, Key id, Map section) { + Map settings = MiscUtils.castToMap(section.get("settings"), true); + if (settings != null) { + Object clientBoundTags = settings.get("client-bound-tags"); + if (clientBoundTags instanceof List list) { + List clientSideTags = MiscUtils.getAsStringList(list).stream().filter(ResourceLocation::isValid).toList(); + try { + Object nmsBlock = Reflections.method$Registry$get.invoke(Reflections.instance$BuiltInRegistries$BLOCK, KeyUtils.toResourceLocation(id)); + FastNMS.INSTANCE.method$IdMap$getId(Reflections.instance$BuiltInRegistries$BLOCK, nmsBlock).ifPresent(i -> + BukkitBlockManager.this.clientBoundTags.put(i, clientSideTags)); + } catch (ReflectiveOperationException e) { + BukkitBlockManager.this.plugin.logger().warn("Unable to get block " + id, e); + } + } + } + } } private Map> parseProperties(Map propertiesSection) { diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/command/feature/TestCommand.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/command/feature/TestCommand.java index 4692de9a3..69bcf57e2 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/command/feature/TestCommand.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/command/feature/TestCommand.java @@ -51,7 +51,6 @@ public class TestCommand extends BukkitCommandFeature { Player player = context.sender(); player.sendMessage("开始测试"); NamespacedKey key = context.get("setTag"); - BlockTags.test(plugin().adapt(player), context.get("reset"), context.get("targetBlock"), key.asString()); player.sendMessage("结束测试"); }); } diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/PacketConsumers.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/PacketConsumers.java index 912bf704e..c35ecd914 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/PacketConsumers.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/PacketConsumers.java @@ -1790,7 +1790,7 @@ public class PacketConsumers { buf.writeLong(seed); } } else { - Optional optionalSound = FastNMS.INSTANCE.method$BuiltInRegistries$byId(Reflections.instance$BuiltInRegistries$SOUND_EVENT, id - 1); + Optional optionalSound = FastNMS.INSTANCE.method$IdMap$byId(Reflections.instance$BuiltInRegistries$SOUND_EVENT, id - 1); if (optionalSound.isEmpty()) return; Object soundEvent = optionalSound.get(); Key soundId = Key.of(FastNMS.INSTANCE.method$SoundEvent$location(soundEvent)); diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/BlockTags.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/BlockTags.java index ef7eb4cd0..e7f746d29 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/BlockTags.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/BlockTags.java @@ -1,16 +1,9 @@ package net.momirealms.craftengine.bukkit.util; -import io.netty.buffer.Unpooled; -import it.unimi.dsi.fastutil.ints.IntArrayList; -import it.unimi.dsi.fastutil.ints.IntList; -import net.momirealms.craftengine.bukkit.nms.FastNMS; -import net.momirealms.craftengine.core.entity.player.Player; -import net.momirealms.craftengine.core.util.FriendlyByteBuf; import net.momirealms.craftengine.core.util.Key; import java.util.HashMap; import java.util.Map; -import java.util.Optional; public class BlockTags { private static final Map CACHE = new HashMap<>(); @@ -31,80 +24,4 @@ public class BlockTags { return value; } } - - /** - * 用于测试下面的 buildFakeUpdateTagsPacket 方法 - * - * @param player CraftEngine玩家对象 - * @param reset 是否重置标签 - * @param targetBlock 测试添加标签的目标方块 - * @param setTag 测试添加的标签 - */ - public static void test(Player player, boolean reset, String targetBlock, String setTag) { - Map> addTags = new HashMap<>(); - if (!reset) { - Object registries = Reflections.instance$BuiltInRegistries$BLOCK; - Object key = FastNMS.INSTANCE.method$Registry$key(registries); - Map blockTags = new HashMap<>(); - IntList blockId = new IntArrayList(); - Object blockKey = KeyUtils.toResourceLocation(Key.of(targetBlock)); - Object block = FastNMS.INSTANCE.method$Registry$get(registries, blockKey); - Optional optionalBlockId = FastNMS.INSTANCE.method$BuiltInRegistries$getId(registries, block); - optionalBlockId.ifPresent(integer -> blockId.add(integer.intValue())); - blockTags.put(setTag, blockId); - addTags.put(key, blockTags); - } - Object packet = buildFakeUpdateTagsPacket(addTags); - player.sendPacket(packet, true); - } - - /** - * 构建模拟标签更新数据包(用于向客户端添加虚拟标签) - * - * @param addTags 需要添加的标签数据,结构为嵌套映射: - *
{@code
-     *               Map结构示例:
-     *               {
-     *                 注册表键1 (如BuiltInRegistries.ITEM.key) -> {
-     *                   "命名空间:值1" -> IntList.of(1, 2, 3),  // 该命名空间下生效的物品ID列表
-     *                   "命名空间:值2" -> IntList.of(5, 7)
-     *                 },
-     *                 注册表键2 (如BuiltInRegistries.BLOCK.key) -> {
-     *                   "minecraft:beacon_base_blocks" -> IntList.of(1024, 2048)
-     *                 },
-     *                 ....
-     *               }
-     *               }
- * 其中:
- * - 外层键:注册表对象(如物品/方块注册表)
- * - 中间层键:标签的命名空间:值(字符串)
- * - 值:包含注册表内项目数字ID的IntList - * - * @return 可发送给客户端的 ClientboundUpdateTagsPacket 数据包对象 - */ - @SuppressWarnings("unchecked") - public static Object buildFakeUpdateTagsPacket(Map> addTags) { - Map registriesNetworkPayload = (Map) FastNMS.INSTANCE.method$TagNetworkSerialization$serializeTagsToNetwork(); - for (Map.Entry> entry : addTags.entrySet()) { - Object registryKey = entry.getKey(); - Map tagsToAdd = entry.getValue(); - Object existingPayload = registriesNetworkPayload.get(registryKey); - if (existingPayload == null) continue; - FriendlyByteBuf deserializeBuf = new FriendlyByteBuf(Unpooled.buffer()); - FastNMS.INSTANCE.method$TagNetworkSerialization$NetworkPayload$write(existingPayload, deserializeBuf); - Map combinedTags = deserializeBuf.readMap( - FriendlyByteBuf::readUtf, - FriendlyByteBuf::readIntIdList - ); - combinedTags.putAll(tagsToAdd); - FriendlyByteBuf serializeBuf = new FriendlyByteBuf(Unpooled.buffer()); - serializeBuf.writeMap(combinedTags, - FriendlyByteBuf::writeUtf, - FriendlyByteBuf::writeIntIdList - ); - Object mergedPayload = FastNMS.INSTANCE.method$TagNetworkSerialization$NetworkPayload$read(serializeBuf); - registriesNetworkPayload.put(registryKey, mergedPayload); - } - return FastNMS.INSTANCE.constructor$ClientboundUpdateTagsPacket(registriesNetworkPayload); - } } diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/KeyUtils.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/KeyUtils.java index 12d203b76..eabcb257d 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/KeyUtils.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/KeyUtils.java @@ -23,4 +23,8 @@ public class KeyUtils { public static Object toResourceLocation(Key key) { return toResourceLocation(key.namespace(), key.value()); } + + public static NamespacedKey toNamespacedKey(Key key) { + return new NamespacedKey(key.namespace(), key.value()); + } } diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/TagUtils.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/TagUtils.java new file mode 100644 index 000000000..e54f30677 --- /dev/null +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/TagUtils.java @@ -0,0 +1,87 @@ +package net.momirealms.craftengine.bukkit.util; + +import io.netty.buffer.Unpooled; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import net.momirealms.craftengine.bukkit.nms.FastNMS; +import net.momirealms.craftengine.core.util.FriendlyByteBuf; +import net.momirealms.craftengine.core.util.Key; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class TagUtils { + + private TagUtils() {} + + /** + * 构建模拟标签更新数据包(用于向客户端添加虚拟标签) + * + * @param tags 需要添加的标签数据,结构为嵌套映射: + *
{@code
+     *               Map结构示例:
+     *               {
+     *                 注册表键1 (如BuiltInRegistries.ITEM.key) -> {
+     *                   "命名空间:值1" -> IntList.of(1, 2, 3),  // 该命名空间下生效的物品ID列表
+     *                   "命名空间:值2" -> IntList.of(5, 7)
+     *                 },
+     *                 注册表键2 (如BuiltInRegistries.BLOCK.key) -> {
+     *                   "minecraft:beacon_base_blocks" -> IntList.of(1024, 2048)
+     *                 },
+     *                 ....
+     *               }
+     *               }
+ * 其中:
+ * - 外层键:注册表ResourceKey
+ * - 中间层键:标签的命名空间:值(字符串)
+ * - 值:包含注册表内项目数字ID的IntList + * + * @return 可发送给客户端的 ClientboundUpdateTagsPacket 数据包对象 + */ + @SuppressWarnings("unchecked") + public static Object createUpdateTagsPacket(Map> tags) { + Map registriesNetworkPayload = (Map) FastNMS.INSTANCE.method$TagNetworkSerialization$serializeTagsToNetwork(); + Map modified = new HashMap<>(); + for (Map.Entry> entry : tags.entrySet()) { + Object existingPayload = registriesNetworkPayload.get(entry.getKey()); + if (existingPayload == null) continue; + FriendlyByteBuf deserializeBuf = new FriendlyByteBuf(Unpooled.buffer()); + FastNMS.INSTANCE.method$TagNetworkSerialization$NetworkPayload$write(existingPayload, deserializeBuf); + Map originalTags = deserializeBuf.readMap( + FriendlyByteBuf::readUtf, + FriendlyByteBuf::readIntIdList + ); + Map> reversedTags = new HashMap<>(); + for (Map.Entry tagEntry : originalTags.entrySet()) { + for (int id : tagEntry.getValue()) { + reversedTags.computeIfAbsent(id, k -> new ArrayList<>()).add(tagEntry.getKey()); + } + } + for (TagEntry tagEntry : entry.getValue()) { + reversedTags.remove(tagEntry.id); + for (String tag : tagEntry.tags) { + reversedTags.computeIfAbsent(tagEntry.id, k -> new ArrayList<>()).add(tag); + } + } + Map processedTags = new HashMap<>(); + for (Map.Entry> tagEntry : reversedTags.entrySet()) { + for (String tag : tagEntry.getValue()) { + processedTags.computeIfAbsent(tag, k -> new IntArrayList()).addLast(tagEntry.getKey()); + } + } + FriendlyByteBuf serializeBuf = new FriendlyByteBuf(Unpooled.buffer()); + serializeBuf.writeMap(processedTags, + FriendlyByteBuf::writeUtf, + FriendlyByteBuf::writeIntIdList + ); + Object mergedPayload = FastNMS.INSTANCE.method$TagNetworkSerialization$NetworkPayload$read(serializeBuf); + modified.put(entry.getKey(), mergedPayload); + } + return FastNMS.INSTANCE.constructor$ClientboundUpdateTagsPacket(modified); + } + + public record TagEntry(int id, List tags) { + } +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/Key.java b/core/src/main/java/net/momirealms/craftengine/core/util/Key.java index ed43ffe8c..994cf4b6c 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/util/Key.java +++ b/core/src/main/java/net/momirealms/craftengine/core/util/Key.java @@ -57,6 +57,10 @@ public record Key(String namespace, String value) { return namespace + ":" + value; } + public String asString() { + return namespace + ":" + value; + } + private static String[] decompose(String id, String namespace) { String[] strings = new String[]{namespace, id}; int i = id.indexOf(':'); diff --git a/gradle.properties b/gradle.properties index 7287d641c..ec72047d6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -50,7 +50,7 @@ byte_buddy_version=1.17.5 ahocorasick_version=0.6.3 snake_yaml_version=2.4 anti_grief_version=0.15 -nms_helper_version=0.65.20 +nms_helper_version=0.65.21 evalex_version=3.5.0 reactive_streams_version=1.0.4 amazon_awssdk_version=2.31.23