9
0
mirror of https://github.com/Xiao-MoMi/craft-engine.git synced 2025-12-31 04:46:37 +00:00

新增客户端侧block tag

This commit is contained in:
XiaoMoMi
2025-05-12 03:01:46 +08:00
parent 40adb5b93f
commit 609cdb65f8
10 changed files with 182 additions and 104 deletions

View File

@@ -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()) {

View File

@@ -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<Integer, List<String>> clientBoundTags = Map.of();
private Map<Integer, List<String>> 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<TagUtils.TagEntry> list = new ArrayList<>();
for (Map.Entry<Integer, List<String>> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> section) {
Map<String, Object> settings = MiscUtils.castToMap(section.get("settings"), true);
if (settings != null) {
Object clientBoundTags = settings.get("client-bound-tags");
if (clientBoundTags instanceof List<?> list) {
List<String> 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<String, Property<?>> parseProperties(Map<String, Object> propertiesSection) {

View File

@@ -51,7 +51,6 @@ public class TestCommand extends BukkitCommandFeature<CommandSender> {
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("结束测试");
});
}

View File

@@ -1790,7 +1790,7 @@ public class PacketConsumers {
buf.writeLong(seed);
}
} else {
Optional<Object> optionalSound = FastNMS.INSTANCE.method$BuiltInRegistries$byId(Reflections.instance$BuiltInRegistries$SOUND_EVENT, id - 1);
Optional<Object> 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));

View File

@@ -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<Key, Object> 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<Object, Map<String, IntList>> addTags = new HashMap<>();
if (!reset) {
Object registries = Reflections.instance$BuiltInRegistries$BLOCK;
Object key = FastNMS.INSTANCE.method$Registry$key(registries);
Map<String, IntList> blockTags = new HashMap<>();
IntList blockId = new IntArrayList();
Object blockKey = KeyUtils.toResourceLocation(Key.of(targetBlock));
Object block = FastNMS.INSTANCE.method$Registry$get(registries, blockKey);
Optional<Integer> 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 需要添加的标签数据,结构为嵌套映射:
* <pre>{@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)
* },
* ....
* }
* }</pre>
* 其中:</br>
* - 外层键:注册表对象(如物品/方块注册表)</br>
* - 中间层键:标签的命名空间:值(字符串)</br>
* - 值包含注册表内项目数字ID的IntList
*
* @return 可发送给客户端的 ClientboundUpdateTagsPacket 数据包对象
*/
@SuppressWarnings("unchecked")
public static Object buildFakeUpdateTagsPacket(Map<Object, Map<String, IntList>> addTags) {
Map<Object, Object> registriesNetworkPayload = (Map<Object, Object>) FastNMS.INSTANCE.method$TagNetworkSerialization$serializeTagsToNetwork();
for (Map.Entry<Object, Map<String, IntList>> entry : addTags.entrySet()) {
Object registryKey = entry.getKey();
Map<String, IntList> 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<String, IntList> 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);
}
}

View File

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

View File

@@ -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 需要添加的标签数据,结构为嵌套映射:
* <pre>{@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)
* },
* ....
* }
* }</pre>
* 其中:</br>
* - 外层键注册表ResourceKey</br>
* - 中间层键:标签的命名空间:值(字符串)</br>
* - 值包含注册表内项目数字ID的IntList
*
* @return 可发送给客户端的 ClientboundUpdateTagsPacket 数据包对象
*/
@SuppressWarnings("unchecked")
public static Object createUpdateTagsPacket(Map<Object, List<TagEntry>> tags) {
Map<Object, Object> registriesNetworkPayload = (Map<Object, Object>) FastNMS.INSTANCE.method$TagNetworkSerialization$serializeTagsToNetwork();
Map<Object, Object> modified = new HashMap<>();
for (Map.Entry<Object, List<TagEntry>> 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<String, IntList> originalTags = deserializeBuf.readMap(
FriendlyByteBuf::readUtf,
FriendlyByteBuf::readIntIdList
);
Map<Integer, List<String>> reversedTags = new HashMap<>();
for (Map.Entry<String, IntList> 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<String, IntList> processedTags = new HashMap<>();
for (Map.Entry<Integer, List<String>> 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<String> tags) {
}
}