diff --git a/eco-api/src/main/java/com/willfp/eco/core/Eco.java b/eco-api/src/main/java/com/willfp/eco/core/Eco.java index 9363842b..e5e919dc 100644 --- a/eco-api/src/main/java/com/willfp/eco/core/Eco.java +++ b/eco-api/src/main/java/com/willfp/eco/core/Eco.java @@ -30,11 +30,13 @@ import com.willfp.eco.core.placeholder.context.PlaceholderContext; import com.willfp.eco.core.proxy.ProxyFactory; import com.willfp.eco.core.scheduling.Scheduler; import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import net.kyori.adventure.text.Component; import org.apache.commons.lang.Validate; import org.bukkit.Location; import org.bukkit.NamespacedKey; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Mob; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -587,6 +589,19 @@ public interface Eco { @NotNull String args, @NotNull PlaceholderContext context); + /** + * Set a client-side entity display name. + * + * @param entity The entity. + * @param player The player. + * @param name The display name. + * @param visible If the display name should be forcibly visible. + */ + void setClientsideDisplayName(@NotNull LivingEntity entity, + @NotNull Player player, + @NotNull Component name, + boolean visible); + /** * Get the instance of eco; the bridge between the api frontend and the implementation backend. * diff --git a/eco-api/src/main/java/com/willfp/eco/util/EntityUtils.java b/eco-api/src/main/java/com/willfp/eco/util/EntityUtils.java new file mode 100644 index 00000000..955693cc --- /dev/null +++ b/eco-api/src/main/java/com/willfp/eco/util/EntityUtils.java @@ -0,0 +1,31 @@ +package com.willfp.eco.util; + +import com.willfp.eco.core.Eco; +import net.kyori.adventure.text.Component; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +/** + * Utilities / API methods for entities. + */ +public final class EntityUtils { + /** + * Set a client-side entity display name. + * + * @param entity The entity. + * @param player The player. + * @param name The display name. + * @param visible If the display name should be forcibly visible. + */ + public static void setClientsideDisplayName(@NotNull final LivingEntity entity, + @NotNull final Player player, + @NotNull final Component name, + final boolean visible) { + Eco.get().setClientsideDisplayName(entity, player, name, visible); + } + + private EntityUtils() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } +} diff --git a/eco-api/src/main/kotlin/com/willfp/eco/util/EntityUtils.kt b/eco-api/src/main/kotlin/com/willfp/eco/util/EntityUtils.kt new file mode 100644 index 00000000..6de26938 --- /dev/null +++ b/eco-api/src/main/kotlin/com/willfp/eco/util/EntityUtils.kt @@ -0,0 +1,12 @@ +@file:JvmName("EntityUtilsExtensions") + +package com.willfp.eco.util + +import net.kyori.adventure.text.Component +import org.bukkit.entity.LivingEntity +import org.bukkit.entity.Player + +/** @see EntityUtils.setClientsideDisplayName */ +fun LivingEntity.setClientsideDisplayName(player: Player, displayName: Component, visible: Boolean) { + EntityUtils.setClientsideDisplayName(this, player, displayName, visible) +} diff --git a/eco-core/core-nms/v1_20_R1/src/main/kotlin/com/willfp/eco/internal/spigot/proxy/v1_20_R1/DisplayName.kt b/eco-core/core-nms/v1_20_R1/src/main/kotlin/com/willfp/eco/internal/spigot/proxy/v1_20_R1/DisplayName.kt new file mode 100644 index 00000000..aed8a740 --- /dev/null +++ b/eco-core/core-nms/v1_20_R1/src/main/kotlin/com/willfp/eco/internal/spigot/proxy/v1_20_R1/DisplayName.kt @@ -0,0 +1,66 @@ +package com.willfp.eco.internal.spigot.proxy.v1_20_R1 + +import com.willfp.eco.core.packet.Packet +import com.willfp.eco.core.packet.sendPacket +import com.willfp.eco.internal.spigot.proxy.DisplayNameProxy +import io.papermc.paper.adventure.PaperAdventure +import net.kyori.adventure.text.Component +import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket +import net.minecraft.network.syncher.EntityDataAccessor +import net.minecraft.network.syncher.SynchedEntityData +import net.minecraft.world.entity.Entity +import org.bukkit.craftbukkit.v1_20_R1.entity.CraftLivingEntity +import org.bukkit.entity.LivingEntity +import org.bukkit.entity.Player +import java.util.Optional + +@Suppress("UNCHECKED_CAST") +class DisplayName : DisplayNameProxy { + private val displayNameAccessor = Entity::class.java + .declaredFields + .filter { it.type == EntityDataAccessor::class.java } + .toList()[2] + .apply { isAccessible = true } + .get(null) as EntityDataAccessor> + + private val customNameVisibleAccessor = Entity::class.java + .declaredFields + .filter { it.type == EntityDataAccessor::class.java } + .toList()[3] + .apply { isAccessible = true } + .get(null) as EntityDataAccessor + + override fun setClientsideDisplayName(entity: LivingEntity, player: Player, displayName: Component, visible: Boolean) { + if (entity !is CraftLivingEntity) { + return + } + + val nmsComponent = PaperAdventure.asVanilla(displayName) + ?: throw IllegalStateException("Display name component is null!") + + val nmsEntity = entity.handle + nmsEntity.isCustomNameVisible + val entityData = SynchedEntityData(nmsEntity) + + entityData.forceSet(displayNameAccessor, Optional.of(nmsComponent)) + entityData.forceSet(customNameVisibleAccessor, visible) + + val packet = ClientboundSetEntityDataPacket( + nmsEntity.id, + entityData.packDirty() ?: throw IllegalStateException("No packed entity data") + ) + + player.sendPacket(Packet(packet)) + } + + private fun SynchedEntityData.forceSet( + accessor: EntityDataAccessor, + value: T + ) { + if (!this.hasItem(accessor)) { + this.define(accessor, value) + } + this[accessor] = value + this.markDirty(accessor) + } +} diff --git a/eco-core/core-nms/v1_20_R2/src/main/kotlin/com/willfp/eco/internal/spigot/proxy/v1_20_R2/DisplayName.kt b/eco-core/core-nms/v1_20_R2/src/main/kotlin/com/willfp/eco/internal/spigot/proxy/v1_20_R2/DisplayName.kt new file mode 100644 index 00000000..121feb52 --- /dev/null +++ b/eco-core/core-nms/v1_20_R2/src/main/kotlin/com/willfp/eco/internal/spigot/proxy/v1_20_R2/DisplayName.kt @@ -0,0 +1,68 @@ +package com.willfp.eco.internal.spigot.proxy.v1_20_R2 + +import com.willfp.eco.core.packet.Packet +import com.willfp.eco.core.packet.sendPacket +import com.willfp.eco.internal.spigot.proxy.DisplayNameProxy +import io.papermc.paper.adventure.PaperAdventure +import net.kyori.adventure.text.Component +import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket +import net.minecraft.network.syncher.EntityDataAccessor +import net.minecraft.network.syncher.SynchedEntityData +import net.minecraft.world.entity.Entity +import org.bukkit.craftbukkit.v1_20_R2.entity.CraftLivingEntity +import org.bukkit.craftbukkit.v1_20_R2.entity.CraftMob +import org.bukkit.entity.LivingEntity +import org.bukkit.entity.Mob +import org.bukkit.entity.Player +import java.util.Optional + +@Suppress("UNCHECKED_CAST") +class DisplayName : DisplayNameProxy { + private val displayNameAccessor = Entity::class.java + .declaredFields + .filter { it.type == EntityDataAccessor::class.java } + .toList()[2] + .apply { isAccessible = true } + .get(null) as EntityDataAccessor> + + private val customNameVisibleAccessor = Entity::class.java + .declaredFields + .filter { it.type == EntityDataAccessor::class.java } + .toList()[3] + .apply { isAccessible = true } + .get(null) as EntityDataAccessor + + override fun setClientsideDisplayName(entity: LivingEntity, player: Player, displayName: Component, visible: Boolean) { + if (entity !is CraftLivingEntity) { + return + } + + val nmsComponent = PaperAdventure.asVanilla(displayName) + ?: throw IllegalStateException("Display name component is null!") + + val nmsEntity = entity.handle + nmsEntity.isCustomNameVisible + val entityData = SynchedEntityData(nmsEntity) + + entityData.forceSet(displayNameAccessor, Optional.of(nmsComponent)) + entityData.forceSet(customNameVisibleAccessor, visible) + + val packet = ClientboundSetEntityDataPacket( + nmsEntity.id, + entityData.packDirty() ?: throw IllegalStateException("No packed entity data") + ) + + player.sendPacket(Packet(packet)) + } + + private fun SynchedEntityData.forceSet( + accessor: EntityDataAccessor, + value: T + ) { + if (!this.hasItem(accessor)) { + this.define(accessor, value) + } + this[accessor] = value + this.markDirty(accessor) + } +} diff --git a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/EcoImpl.kt b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/EcoImpl.kt index 6cfa89fc..539c717a 100644 --- a/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/EcoImpl.kt +++ b/eco-core/core-plugin/src/main/kotlin/com/willfp/eco/internal/spigot/EcoImpl.kt @@ -4,6 +4,7 @@ import com.willfp.eco.core.Eco import com.willfp.eco.core.EcoPlugin import com.willfp.eco.core.PluginLike import com.willfp.eco.core.PluginProps +import com.willfp.eco.core.Prerequisite import com.willfp.eco.core.command.CommandBase import com.willfp.eco.core.command.PluginCommandBase import com.willfp.eco.core.config.ConfigType @@ -51,6 +52,7 @@ import com.willfp.eco.internal.spigot.math.ImmediatePlaceholderTranslationExpres import com.willfp.eco.internal.spigot.math.LazyPlaceholderTranslationExpressionHandler import com.willfp.eco.internal.spigot.proxy.BukkitCommandsProxy import com.willfp.eco.internal.spigot.proxy.CommonsInitializerProxy +import com.willfp.eco.internal.spigot.proxy.DisplayNameProxy import com.willfp.eco.internal.spigot.proxy.DummyEntityFactoryProxy import com.willfp.eco.internal.spigot.proxy.EntityControllerFactoryProxy import com.willfp.eco.internal.spigot.proxy.ExtendedPersistentDataContainerFactoryProxy @@ -60,10 +62,12 @@ import com.willfp.eco.internal.spigot.proxy.PacketHandlerProxy import com.willfp.eco.internal.spigot.proxy.SNBTConverterProxy import com.willfp.eco.internal.spigot.proxy.SkullProxy import com.willfp.eco.internal.spigot.proxy.TPSProxy +import net.kyori.adventure.text.Component import org.bukkit.Location import org.bukkit.NamespacedKey import org.bukkit.configuration.ConfigurationSection import org.bukkit.entity.Entity +import org.bukkit.entity.LivingEntity import org.bukkit.entity.Mob import org.bukkit.entity.Player import org.bukkit.inventory.ItemStack @@ -346,4 +350,9 @@ class EcoImpl : EcoSpigotPlugin(), Eco { override fun getPlaceholderValue(plugin: EcoPlugin?, args: String, context: PlaceholderContext) = placeholderParser.getPlaceholderResult(plugin, args, context) + + override fun setClientsideDisplayName(entity: LivingEntity, player: Player, name: Component, visible: Boolean) = + if (Prerequisite.HAS_PAPER.isMet && Prerequisite.HAS_1_20.isMet) + this.getProxy(DisplayNameProxy::class.java).setClientsideDisplayName(entity, player, name, visible) + else Unit } diff --git a/eco-core/core-proxy/src/main/kotlin/com/willfp/eco/internal/spigot/proxy/DisplayNameProxy.kt b/eco-core/core-proxy/src/main/kotlin/com/willfp/eco/internal/spigot/proxy/DisplayNameProxy.kt new file mode 100644 index 00000000..a8ebae5b --- /dev/null +++ b/eco-core/core-proxy/src/main/kotlin/com/willfp/eco/internal/spigot/proxy/DisplayNameProxy.kt @@ -0,0 +1,9 @@ +package com.willfp.eco.internal.spigot.proxy + +import net.kyori.adventure.text.Component +import org.bukkit.entity.LivingEntity +import org.bukkit.entity.Player + +interface DisplayNameProxy { + fun setClientsideDisplayName(entity: LivingEntity, player: Player, displayName: Component, visible: Boolean) +}