From a842a4abec7b027dc7cc5d7a2b12f4dfcaadd1ed Mon Sep 17 00:00:00 2001 From: iqtester Date: Thu, 3 Apr 2025 22:06:56 +0800 Subject: [PATCH] Implement totem animation feature --- bukkit/loader/src/main/resources/commands.yml | 7 ++ .../src/main/resources/translations/en.yml | 3 +- .../src/main/resources/translations/es.yml | 3 +- .../src/main/resources/translations/zh_cn.yml | 3 +- .../src/main/resources/translations/zh_tw.yml | 3 +- .../bukkit/item/BukkitItemManager.java | 2 + .../plugin/command/BukkitCommandManager.java | 3 +- .../plugin/command/feature/TotemCommand.java | 73 +++++++++++++++++++ .../craftengine/bukkit/util/PlayerUtils.java | 31 ++++++++ .../craftengine/bukkit/util/Reflections.java | 26 +++++++ .../core/item/AbstractItemManager.java | 7 ++ .../craftengine/core/item/ItemManager.java | 2 + .../core/plugin/locale/MessageConstants.java | 1 + 13 files changed, 159 insertions(+), 5 deletions(-) create mode 100644 bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/command/feature/TotemCommand.java diff --git a/bukkit/loader/src/main/resources/commands.yml b/bukkit/loader/src/main/resources/commands.yml index 1e4acc716..1e959b731 100644 --- a/bukkit/loader/src/main/resources/commands.yml +++ b/bukkit/loader/src/main/resources/commands.yml @@ -69,6 +69,13 @@ search_recipe_admin: - /craftengine item search-recipe - /ce item search-recipe +totem: + enable: true + permission: ce.command.admin.totem + usage: + - /craftengine totem + - /ce totem + # Debug commands debug_set_block: enable: true diff --git a/bukkit/loader/src/main/resources/translations/en.yml b/bukkit/loader/src/main/resources/translations/en.yml index eab86ce3f..7ddd21f54 100644 --- a/bukkit/loader/src/main/resources/translations/en.yml +++ b/bukkit/loader/src/main/resources/translations/en.yml @@ -52,4 +52,5 @@ command.item.give.failure.not_exist: "No recipe found for this item" command.search_usage.not_found: "No usage found for this item" command.search_recipe.no_item: "Please hold an item before running this command" -command.search_usage.no_item: "Please hold an item before running this command" \ No newline at end of file +command.search_usage.no_item: "Please hold an item before running this command" +command.totem.not_totem: "'' is not type of totem_of_undying" \ No newline at end of file diff --git a/bukkit/loader/src/main/resources/translations/es.yml b/bukkit/loader/src/main/resources/translations/es.yml index b716978d9..23de3a017 100644 --- a/bukkit/loader/src/main/resources/translations/es.yml +++ b/bukkit/loader/src/main/resources/translations/es.yml @@ -52,4 +52,5 @@ command.item.give.failure.not_exist: "No se encontró ninguna receta para este objeto" command.search_usage.not_found: "No se encontró ningún uso para este objeto" command.search_recipe.no_item: "Por favor, sostén un objeto antes de ejecutar este comando" -command.search_usage.no_item: "Por favor, sostén un objeto antes de ejecutar este comando" \ No newline at end of file +command.search_usage.no_item: "Por favor, sostén un objeto antes de ejecutar este comando" +command.totem.not_totem: "'' no es del tipo totem_of_undying" \ No newline at end of file diff --git a/bukkit/loader/src/main/resources/translations/zh_cn.yml b/bukkit/loader/src/main/resources/translations/zh_cn.yml index 3de54b7d3..29cb7a370 100644 --- a/bukkit/loader/src/main/resources/translations/zh_cn.yml +++ b/bukkit/loader/src/main/resources/translations/zh_cn.yml @@ -52,4 +52,5 @@ command.item.give.failure.not_exist: "找不到此物品的配方" command.search_usage.not_found: "找不到此物品的用途" command.search_recipe.no_item: "请手持物品后再执行此命令" -command.search_usage.no_item: "请手持物品后再执行此命令" \ No newline at end of file +command.search_usage.no_item: "请手持物品后再执行此命令" +command.totem.not_totem: "'' 不是 totem_of_undying 类型" \ No newline at end of file diff --git a/bukkit/loader/src/main/resources/translations/zh_tw.yml b/bukkit/loader/src/main/resources/translations/zh_tw.yml index 8a48c57d8..37aae210f 100644 --- a/bukkit/loader/src/main/resources/translations/zh_tw.yml +++ b/bukkit/loader/src/main/resources/translations/zh_tw.yml @@ -52,4 +52,5 @@ command.item.give.failure.not_exist: "找不到此物品的配方" command.search_usage.not_found: "找不到此物品的用途" command.search_recipe.no_item: "執行此命令前請手持物品" -command.search_usage.no_item: "執行此命令前請手持物品" \ No newline at end of file +command.search_usage.no_item: "執行此命令前請手持物品" +command.totem.not_totem: "'' 不是 totem_of_undying 類型" \ No newline at end of file diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/BukkitItemManager.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/BukkitItemManager.java index bba404771..ef2df937e 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/BukkitItemManager.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/item/BukkitItemManager.java @@ -256,6 +256,8 @@ public class BukkitItemManager extends AbstractItemManager { CustomItem customItem = itemBuilder.build(); this.customItems.put(id, customItem); this.cachedSuggestions.add(Suggestion.suggestion(id.toString())); + if (material == Material.TOTEM_OF_UNDYING) + this.cachedTotemSuggestions.add(Suggestion.suggestion(id.toString())); // post process // register tags 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 ec38a6f81..f83af185c 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 @@ -45,7 +45,8 @@ public class BukkitCommandManager extends AbstractCommandManager new DebugItemDataCommand(this, plugin), new DebugSetBlockCommand(this, plugin), new DebugSpawnFurnitureCommand(this, plugin), - new DebugTargetBlockCommand(this, plugin) + new DebugTargetBlockCommand(this, plugin), + new TotemCommand(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/TotemCommand.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/command/feature/TotemCommand.java new file mode 100644 index 000000000..72e8a64db --- /dev/null +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/command/feature/TotemCommand.java @@ -0,0 +1,73 @@ +package net.momirealms.craftengine.bukkit.plugin.command.feature; + +import net.kyori.adventure.text.Component; +import net.momirealms.craftengine.bukkit.plugin.command.BukkitCommandFeature; +import net.momirealms.craftengine.bukkit.util.MaterialUtils; +import net.momirealms.craftengine.bukkit.util.PlayerUtils; +import net.momirealms.craftengine.core.item.CustomItem; +import net.momirealms.craftengine.core.plugin.CraftEngine; +import net.momirealms.craftengine.core.plugin.command.CraftEngineCommandManager; +import net.momirealms.craftengine.core.plugin.command.FlagKeys; +import net.momirealms.craftengine.core.plugin.locale.MessageConstants; +import net.momirealms.craftengine.core.util.Key; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.Command; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.bukkit.data.MultiplePlayerSelector; +import org.incendo.cloud.bukkit.parser.NamespacedKeyParser; +import org.incendo.cloud.bukkit.parser.selector.MultiplePlayerSelectorParser; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.context.CommandInput; +import org.incendo.cloud.suggestion.Suggestion; +import org.incendo.cloud.suggestion.SuggestionProvider; + +import java.util.concurrent.CompletableFuture; + +public class TotemCommand extends BukkitCommandFeature { + + public TotemCommand(CraftEngineCommandManager commandManager, CraftEngine plugin) { + super(commandManager, plugin); + } + + @Override + public Command.Builder assembleCommand(CommandManager manager, Command.Builder builder) { + return builder + .flag(FlagKeys.SILENT_FLAG) + .required("players", MultiplePlayerSelectorParser.multiplePlayerSelectorParser()) + .required("id", NamespacedKeyParser.namespacedKeyComponent().suggestionProvider(new SuggestionProvider<>() { + @Override + public @NonNull CompletableFuture> suggestionsFuture(@NonNull CommandContext context, @NonNull CommandInput input) { + return CompletableFuture.completedFuture(plugin().itemManager().cachedTotemSuggestions()); + } + })) + .handler(context -> { + NamespacedKey namespacedKey = context.get("id"); + Key key = Key.of(namespacedKey.namespace(), namespacedKey.value()); + CustomItem item = plugin().itemManager().getCustomItem(key).orElse(null); + if (item == null) { + handleFeedback(context, MessageConstants.COMMAND_ITEM_GET_FAILURE_NOT_EXIST, Component.text(key.toString())); + return; + } + if (MaterialUtils.getMaterial(item.material()) != Material.TOTEM_OF_UNDYING) { + handleFeedback(context, MessageConstants.COMMAND_TOTEM_NOT_TOTEM, Component.text(key.toString())); + return; + } + + ItemStack totem = item.buildItemStack(); + MultiplePlayerSelector selector = context.get("players"); + for (Player player : selector.values()) { + PlayerUtils.sendTotemAnimation(player, totem); + } + }); + } + + @Override + public String getFeatureID() { + return "totem"; + } +} diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/PlayerUtils.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/PlayerUtils.java index 0030b1a93..dfe9436ff 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/PlayerUtils.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/PlayerUtils.java @@ -1,5 +1,9 @@ package net.momirealms.craftengine.bukkit.util; +import com.mojang.datafixers.util.Pair; +import net.momirealms.craftengine.bukkit.nms.FastNMS; +import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine; +import net.momirealms.craftengine.bukkit.plugin.network.BukkitNetworkManager; import net.momirealms.craftengine.core.util.RandomUtils; import org.bukkit.Location; import org.bukkit.entity.Item; @@ -11,6 +15,9 @@ import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.util.Vector; import org.jetbrains.annotations.NotNull; +import java.util.ArrayList; +import java.util.List; + import static java.util.Objects.requireNonNull; public class PlayerUtils { @@ -140,4 +147,28 @@ public class PlayerUtils { return actualAmount; } + + public static void sendTotemAnimation(Player player, ItemStack totem) { + ItemStack offhandItem = player.getInventory().getItemInOffHand(); + List packets = new ArrayList<>(); + try { + Object previousItem = Reflections.method$CraftItemStack$asNMSCopy.invoke(null, offhandItem); + Object totemItem = Reflections.method$CraftItemStack$asNMSCopy.invoke(null, totem); + + Object packet1 = Reflections.constructor$ClientboundSetEquipmentPacket + .newInstance(player.getEntityId(), List.of(Pair.of(Reflections.instance$EquipmentSlot$OFFHAND, totemItem))); + Object packet2 = Reflections.constructor$ClientboundEntityEventPacket + .newInstance(FastNMS.INSTANCE.method$CraftPlayer$getHandle(player), (byte) 35); + Object packet3 = Reflections.constructor$ClientboundSetEquipmentPacket + .newInstance(player.getEntityId(), List.of(Pair.of(Reflections.instance$EquipmentSlot$OFFHAND, previousItem))); + packets.add(packet1); + packets.add(packet2); + packets.add(packet3); + + Object bundlePacket = FastNMS.INSTANCE.constructor$ClientboundBundlePacket(packets); + BukkitNetworkManager.instance().sendPacket(player, bundlePacket); + } catch (ReflectiveOperationException e) { + BukkitCraftEngine.instance().logger().warn("Failed to send totem animation"); + } + } } diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/Reflections.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/Reflections.java index 0a1abb9ca..04f752199 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/Reflections.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/util/Reflections.java @@ -3033,6 +3033,32 @@ public class Reflections { } } + public static final Class clazz$ClientboundSetEquipmentPacket = requireNonNull( + ReflectionUtils.getClazz( + BukkitReflectionUtils.assembleMCClass("network.protocol.game.ClientboundSetEquipmentPacket"), + BukkitReflectionUtils.assembleMCClass("network.protocol.game.PacketPlayOutEntityEquipment") + ) + ); + + public static final Constructor constructor$ClientboundSetEquipmentPacket = requireNonNull( + ReflectionUtils.getConstructor( + clazz$ClientboundSetEquipmentPacket, int.class, List.class + ) + ); + + public static final Class clazz$ClientboundEntityEventPacket = requireNonNull( + ReflectionUtils.getClazz( + BukkitReflectionUtils.assembleMCClass("network.protocol.game.ClientboundEntityEventPacket"), + BukkitReflectionUtils.assembleMCClass("network.protocol.game.PacketPlayOutEntityStatus") + ) + ); + + public static final Constructor constructor$ClientboundEntityEventPacket = requireNonNull( + ReflectionUtils.getConstructor( + clazz$ClientboundEntityEventPacket, clazz$Entity, byte.class + ) + ); + public static final Method method$Block$defaultBlockState = requireNonNull( ReflectionUtils.getMethod( clazz$Block, clazz$BlockState 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 eb8d76b67..f796f74c9 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 @@ -35,6 +35,7 @@ public abstract class AbstractItemManager extends AbstractModelGenerator impl protected final Set equipmentsToGenerate; // Cached command suggestions protected final List cachedSuggestions = new ArrayList<>(); + protected final List cachedTotemSuggestions = new ArrayList<>(); protected void registerDataFunction(Function> function, String... alias) { for (String a : alias) { @@ -65,6 +66,7 @@ public abstract class AbstractItemManager extends AbstractModelGenerator impl super.clearModelsToGenerate(); this.customItems.clear(); this.cachedSuggestions.clear(); + this.cachedTotemSuggestions.clear(); this.legacyOverrides.clear(); this.modernOverrides.clear(); this.customItemTags.clear(); @@ -120,6 +122,11 @@ public abstract class AbstractItemManager extends AbstractModelGenerator impl return Collections.unmodifiableCollection(this.cachedSuggestions); } + @Override + public Collection cachedTotemSuggestions() { + return Collections.unmodifiableCollection(this.cachedTotemSuggestions); + } + @Override public Optional> getItemBehavior(Key key) { Optional> customItemOptional = getCustomItem(key); diff --git a/core/src/main/java/net/momirealms/craftengine/core/item/ItemManager.java b/core/src/main/java/net/momirealms/craftengine/core/item/ItemManager.java index 5279003cd..c032cb13e 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/item/ItemManager.java +++ b/core/src/main/java/net/momirealms/craftengine/core/item/ItemManager.java @@ -85,5 +85,7 @@ public interface ItemManager extends Reloadable, ModelGenerator, ConfigSectio Collection cachedSuggestions(); + Collection cachedTotemSuggestions(); + Object encodeJava(Key componentType, @Nullable Object component); } \ No newline at end of file diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/locale/MessageConstants.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/locale/MessageConstants.java index ec3b8b640..bef7dd276 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/plugin/locale/MessageConstants.java +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/locale/MessageConstants.java @@ -20,4 +20,5 @@ public interface MessageConstants { TranslatableComponent.Builder COMMAND_SEARCH_RECIPE_NO_ITEM = Component.translatable().key("command.search_recipe.no_item"); TranslatableComponent.Builder COMMAND_SEARCH_USAGE_NOT_FOUND = Component.translatable().key("command.search_usage.not_found"); TranslatableComponent.Builder COMMAND_SEARCH_USAGE_NO_ITEM = Component.translatable().key("command.search_usage.no_item"); + TranslatableComponent.Builder COMMAND_TOTEM_NOT_TOTEM = Component.translatable().key("command.totem.not_totem"); }