diff --git a/build.gradle.kts b/build.gradle.kts index b886114..345b5ab 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,6 +34,9 @@ dependencies { api("com.squareup.okhttp3:okhttp:4.12.0") + // PacketEvents for TextDisplayFactory (display entities) + api("com.github.retrooper:packetevents-spigot:2.10.1") + // Redis client for Redis-backed PartyAPI implementation (Jedis) implementation("redis.clients:jedis:7.1.0") diff --git a/src/main/kotlin/cc/modlabs/kpaper/visuals/display/BlockDisplayManager.kt b/src/main/kotlin/cc/modlabs/kpaper/visuals/display/BlockDisplayManager.kt new file mode 100644 index 0000000..91fda2b --- /dev/null +++ b/src/main/kotlin/cc/modlabs/kpaper/visuals/display/BlockDisplayManager.kt @@ -0,0 +1,552 @@ +package cc.modlabs.kpaper.visuals.display + +import com.github.retrooper.packetevents.PacketEvents +import com.github.retrooper.packetevents.protocol.entity.data.EntityData +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes +import com.github.retrooper.packetevents.protocol.world.states.WrappedBlockState +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerDestroyEntities +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityMetadata +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSpawnEntity +import com.github.retrooper.packetevents.util.Vector3d +import com.github.retrooper.packetevents.util.Vector3f +import org.bukkit.Location +import org.bukkit.block.data.BlockData +import org.bukkit.entity.Player +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicInteger + +/** + * A manager class for creating and managing packet-based Block Display entities in Minecraft. + * + * Block Displays are client-side entities that can display blocks at specific locations. + * This manager handles the lifecycle of these displays, including creation, updates, and removal. + * + * @example + * ```kotlin + * val manager = BlockDisplayManager() + * val display = manager.createBlockDisplay( + * blockData = Bukkit.createBlockData(Material.DIAMOND_BLOCK), + * location = player.location, + * viewer = player, + * billboard = BlockDisplayManager.BlockDisplayBillboard.CENTER, + * glow = true, + * scale = Vector3f(1.0f, 1.0f, 1.0f), + * displayTransformation = BlockDisplayManager.BlockDisplayTransformation.NONE + * ) + * ``` + */ +class BlockDisplayManager { + + /** + * Stores all active Block Displays, mapped by Entity ID. + */ + private val activeDisplays = ConcurrentHashMap() + + /** + * Counter for generating unique Entity IDs. + * Starts at 3000000 to avoid conflicts with regular entities, Text Displays, and Item Displays. + */ + private val nextEntityId = AtomicInteger(3000000) + + /** + * Creates a Block Display for a specific player. + * + * @param blockData The block data to display + * @param location The position of the Block Display + * @param viewer The player who should see the Block Display + * @param billboard The billboard mode for the display + * @param glow Whether the display should glow (outline effect) + * @param scale The scale of the block (default: 1.0f, 1.0f, 1.0f) + * @param displayTransformation How the block should be displayed (default: NONE) + * @return The created BlockDisplay object + */ + fun createBlockDisplay( + blockData: BlockData, + location: Location, + viewer: Player, + billboard: BlockDisplayBillboard, + glow: Boolean = false, + scale: Vector3f = Vector3f(1.0f, 1.0f, 1.0f), + displayTransformation: BlockDisplayTransformation = BlockDisplayTransformation.NONE + ): BlockDisplay { + val entityId = nextEntityId.getAndIncrement() + val blockDisplay = BlockDisplay( + entityId, blockData, location.clone(), mutableSetOf(viewer), billboard, glow, scale, displayTransformation + ) + + spawnBlockDisplay(blockDisplay, viewer) + activeDisplays[entityId] = blockDisplay + + return blockDisplay + } + + /** + * Creates a Block Display for multiple players. + * + * @param blockData The block data to display + * @param location The position of the Block Display + * @param viewers The players who should see the Block Display + * @param billboard The billboard mode for the display + * @param glow Whether the display should glow (outline effect) + * @param scale The scale of the block (default: 1.0f, 1.0f, 1.0f) + * @param displayTransformation How the block should be displayed (default: NONE) + * @return The created BlockDisplay object + */ + fun createBlockDisplay( + blockData: BlockData, + location: Location, + viewers: Collection, + billboard: BlockDisplayBillboard, + glow: Boolean = false, + scale: Vector3f = Vector3f(1.0f, 1.0f, 1.0f), + displayTransformation: BlockDisplayTransformation = BlockDisplayTransformation.NONE + ): BlockDisplay { + val entityId = nextEntityId.getAndIncrement() + val blockDisplay = BlockDisplay( + entityId, blockData, location.clone(), viewers.toMutableSet(), billboard, glow, scale, displayTransformation + ) + + for (viewer in viewers) { + spawnBlockDisplay(blockDisplay, viewer) + } + + activeDisplays[entityId] = blockDisplay + return blockDisplay + } + + /** + * Shows an existing Block Display to an additional player. + * + * @param blockDisplay The BlockDisplay to show + * @param viewer The player who should see the Block Display + * @return `true` if the viewer was added, `false` if they could already see the display + */ + fun addViewer(blockDisplay: BlockDisplay, viewer: Player): Boolean { + if (blockDisplay.viewers.add(viewer)) { + spawnBlockDisplay(blockDisplay, viewer) + return true + } + return false + } + + /** + * Hides a Block Display for a specific player. + * + * @param blockDisplay The BlockDisplay to hide + * @param viewer The player for whom the display should be hidden + * @return `true` if the viewer was removed, `false` if they couldn't see the display + */ + fun removeViewer(blockDisplay: BlockDisplay, viewer: Player): Boolean { + if (blockDisplay.viewers.remove(viewer)) { + destroyEntity(viewer, blockDisplay.entityId) + + // If no viewers remain, remove the display completely + if (blockDisplay.viewers.isEmpty()) { + activeDisplays.remove(blockDisplay.entityId) + } + + return true + } + return false + } + + /** + * Updates the block data of an existing Block Display. + * + * @param blockDisplay The BlockDisplay to update + * @param newBlockData The new block data to display + */ + fun updateBlock(blockDisplay: BlockDisplay, newBlockData: BlockData) { + blockDisplay.blockData = newBlockData + + // Send metadata update for all viewers + val metadataPacket = createMetadataPacket(blockDisplay) + for (viewer in blockDisplay.viewers) { + PacketEvents.getAPI().playerManager.sendPacket(viewer, metadataPacket) + } + } + + /** + * Updates the scale of an existing Block Display. + * + * @param blockDisplay The BlockDisplay to update + * @param newScale The new scale + */ + fun updateScale(blockDisplay: BlockDisplay, newScale: Vector3f) { + blockDisplay.scale = newScale + + val metadataPacket = createMetadataPacket(blockDisplay) + for (viewer in blockDisplay.viewers) { + PacketEvents.getAPI().playerManager.sendPacket(viewer, metadataPacket) + } + } + + /** + * Updates the transformation type of an existing Block Display. + * + * @param blockDisplay The BlockDisplay to update + * @param newTransformation The new transformation type + */ + fun updateTransformation(blockDisplay: BlockDisplay, newTransformation: BlockDisplayTransformation) { + blockDisplay.displayTransformation = newTransformation + + val metadataPacket = createMetadataPacket(blockDisplay) + for (viewer in blockDisplay.viewers) { + PacketEvents.getAPI().playerManager.sendPacket(viewer, metadataPacket) + } + } + + /** + * Updates the position of an existing Block Display. + * + * Note: This requires respawning the entity for all viewers, which may cause a brief flicker. + * + * @param blockDisplay The BlockDisplay to update + * @param newLocation The new position + */ + fun updateLocation(blockDisplay: BlockDisplay, newLocation: Location) { + blockDisplay.location = newLocation.clone() + + // For all viewers, respawn the display (there's no simple packet for position updates) + for (viewer in blockDisplay.viewers.toSet()) { // Create copy to avoid ConcurrentModification + destroyEntity(viewer, blockDisplay.entityId) + spawnBlockDisplay(blockDisplay, viewer) + } + } + + /** + * Removes a Block Display completely. + * + * @param blockDisplay The BlockDisplay to remove + */ + fun removeBlockDisplay(blockDisplay: BlockDisplay) { + activeDisplays.remove(blockDisplay.entityId) + + for (viewer in blockDisplay.viewers.toSet()) { + destroyEntity(viewer, blockDisplay.entityId) + } + + blockDisplay.viewers.clear() + } + + /** + * Removes all Block Displays. + */ + fun removeAllBlockDisplays() { + for (blockDisplay in activeDisplays.values.toList()) { + removeBlockDisplay(blockDisplay) + } + } + + /** + * Gets a BlockDisplay by its entity ID. + * + * @param entityId The entity ID of the BlockDisplay + * @return The BlockDisplay or `null` if it doesn't exist + */ + operator fun get(entityId: Int): BlockDisplay? { + return activeDisplays[entityId] + } + + /** + * Gets all active BlockDisplays. + * + * @return A collection of all active BlockDisplays + */ + fun getAllBlockDisplays(): Collection { + return activeDisplays.values + } + + /** + * Checks if a specific entity ID belongs to a BlockDisplay. + * + * @param entityId The entity ID to check + * @return `true` if a BlockDisplay with this ID exists + */ + fun isBlockDisplay(entityId: Int): Boolean { + return activeDisplays.containsKey(entityId) + } + + /** + * Sends the packets to spawn a Block Display for a player. + * + * @param blockDisplay The BlockDisplay to spawn + * @param viewer The player who should see the display + */ + private fun spawnBlockDisplay(blockDisplay: BlockDisplay, viewer: Player) { + try { + // 1. Spawn Entity Packet + val location = blockDisplay.location + val spawnPacket = WrapperPlayServerSpawnEntity( + blockDisplay.entityId, + Optional.of(UUID.randomUUID()), + EntityTypes.BLOCK_DISPLAY, + Vector3d(location.x, location.y + blockDisplay.yOffset, location.z), + location.pitch.toFloat(), + location.yaw.toFloat(), + location.yaw.toFloat(), + 0, + Optional.of(Vector3d(0.0, 0.0, 0.0)) + ) + + // 2. Entity Metadata Packet + val metadataPacket = createMetadataPacket(blockDisplay) + + // Send packets + PacketEvents.getAPI().playerManager.sendPacket(viewer, spawnPacket) + PacketEvents.getAPI().playerManager.sendPacket(viewer, metadataPacket) + } catch (e: Exception) { + // In case of an error, log it instead of crashing the plugin + e.printStackTrace() + } + } + + /** + * Creates a metadata packet for a Block Display. + * + * @param blockDisplay The BlockDisplay + * @return The created EntityMetadata packet + */ + private fun createMetadataPacket(blockDisplay: BlockDisplay): WrapperPlayServerEntityMetadata { + val metadataList = ArrayList>() + + // Billboard mode (index 15) + metadataList.add(EntityData(15, EntityDataTypes.BYTE, blockDisplay.billboard.billboardValue.toByte())) + + // Glow effect (index 22) + metadataList.add(EntityData(22, EntityDataTypes.INT, blockDisplay.glowingInt())) + + // The block state itself - Conversion from Bukkit BlockData to WrappedBlockState + val blockState = WrappedBlockState.getByString(blockDisplay.blockData.asString) + metadataList.add(EntityData(23, EntityDataTypes.BLOCK_STATE, blockState.typeData.ordinal)) + + // Display transformation (index 24) - how the block is displayed + metadataList.add(EntityData(24, EntityDataTypes.BYTE, blockDisplay.displayTransformation.value.toByte())) + + // Scale of the block (index 12) + metadataList.add(EntityData(12, EntityDataTypes.VECTOR3F, blockDisplay.scale)) + + return WrapperPlayServerEntityMetadata(blockDisplay.entityId, metadataList) + } + + /** + * Sends a Destroy-Entity packet to a player. + * + * @param player The player + * @param entityId The entity ID to destroy + */ + internal fun destroyEntity(player: Player, entityId: Int) { + val destroyPacket = WrapperPlayServerDestroyEntities(entityId) + PacketEvents.getAPI().playerManager.sendPacket(player, destroyPacket) + } + + /** + * Billboard modes for Block Displays. + * Controls how the block display rotates relative to the viewer. + */ + enum class BlockDisplayBillboard(val billboardValue: Int) { + /** + * Fixed orientation - doesn't rotate, always faces the same direction. + */ + FIXED(0), + + /** + * Vertical rotation - rotates around Y axis to face the player horizontally. + */ + VERTICAL(1), + + /** + * Horizontal rotation - rotates around X axis to face the player vertically. + */ + HORIZONTAL(2), + + /** + * Center rotation - rotates to fully face the player (both axes). + */ + CENTER(3) + } + + /** + * Display transformation modes for Block Displays. + * Controls how the block is rendered (perspective, position, etc.). + */ + enum class BlockDisplayTransformation(val value: Int) { + /** + * No transformation - default display. + */ + NONE(0), + + /** + * Third person left hand perspective. + */ + THIRDPERSON_LEFTHAND(1), + + /** + * Third person right hand perspective. + */ + THIRDPERSON_RIGHTHAND(2), + + /** + * First person left hand perspective. + */ + FIRSTPERSON_LEFTHAND(3), + + /** + * First person right hand perspective. + */ + FIRSTPERSON_RIGHTHAND(4), + + /** + * Head/helmet perspective. + */ + HEAD(5), + + /** + * GUI/inventory perspective. + */ + GUI(6), + + /** + * Ground/dropped block perspective. + */ + GROUND(7), + + /** + * Fixed perspective. + */ + FIXED(8) + } + + /** + * Represents a Block Display entity. + * + * @property entityId The unique entity ID assigned to this display + * @property blockData The block data being displayed + * @property location The current location of the display + * @property viewers The set of players who can currently see this display + * @property billboard The billboard mode for rotation behavior + * @property glowing Whether the display has a glow effect + * @property scale The scale of the block display + * @property displayTransformation How the block is displayed + */ + class BlockDisplay( + val entityId: Int, + var blockData: BlockData, + var location: Location, + val viewers: MutableSet, + val billboard: BlockDisplayBillboard, + val glowing: Boolean = false, + var scale: Vector3f = Vector3f(1.0f, 1.0f, 1.0f), + var displayTransformation: BlockDisplayTransformation = BlockDisplayTransformation.NONE + ) { + /** + * Vertical offset from the base location. + * How far above the ground/position the display should appear. + */ + var yOffset: Double = 0.0 + + /** + * Sets the block data of the display. + * Shortcut for [BlockDisplayManager.updateBlock]. + * + * @param manager The manager instance to use for the update + * @param newBlockData The new block data to display + */ + fun setBlock(manager: BlockDisplayManager, newBlockData: BlockData) { + manager.updateBlock(this, newBlockData) + } + + /** + * Gets the glow effect value for the entity metadata. + * + * @return 10 if glowing, -1 otherwise + */ + fun glowingInt(): Int { + return if (glowing) { + 10 + } else { + -1 + } + } + + /** + * Changes the scale of the display. + * Shortcut for [BlockDisplayManager.updateScale]. + * + * @param manager The manager instance to use for the update + * @param newScale The new scale + */ + fun setScale(manager: BlockDisplayManager, newScale: Vector3f) { + manager.updateScale(this, newScale) + } + + /** + * Changes the transformation type of the display. + * Shortcut for [BlockDisplayManager.updateTransformation]. + * + * @param manager The manager instance to use for the update + * @param newTransformation The new transformation type + */ + fun setTransformation(manager: BlockDisplayManager, newTransformation: BlockDisplayTransformation) { + manager.updateTransformation(this, newTransformation) + } + + /** + * Moves the display to a new position. + * Shortcut for [BlockDisplayManager.updateLocation]. + * + * @param manager The manager instance to use for the update + * @param newLocation The new position + */ + fun moveToLocation(manager: BlockDisplayManager, newLocation: Location) { + manager.updateLocation(this, newLocation) + } + + /** + * Adds a new viewer. + * Shortcut for [BlockDisplayManager.addViewer]. + * + * @param manager The manager instance to use + * @param viewer The player to add as a viewer + * @return `true` if the viewer was added, `false` if they could already see it + */ + fun addViewer(manager: BlockDisplayManager, viewer: Player): Boolean { + return manager.addViewer(this, viewer) + } + + /** + * Removes a viewer. + * Shortcut for [BlockDisplayManager.removeViewer]. + * + * @param manager The manager instance to use + * @param viewer The player to remove as a viewer + * @return `true` if the viewer was removed, `false` if they couldn't see it + */ + fun removeViewer(manager: BlockDisplayManager, viewer: Player): Boolean { + return manager.removeViewer(this, viewer) + } + + /** + * Removes this display. + * Shortcut for [BlockDisplayManager.removeBlockDisplay]. + * + * @param manager The manager instance to use + */ + fun remove(manager: BlockDisplayManager) { + manager.removeBlockDisplay(this) + } + + /** + * Checks if the display is visible to a specific player. + * + * @param player The player to check + * @return `true` if the player can see the display + */ + fun isVisibleTo(player: Player): Boolean { + return player in viewers + } + } +} + diff --git a/src/main/kotlin/cc/modlabs/kpaper/visuals/display/ItemDisplayManager.kt b/src/main/kotlin/cc/modlabs/kpaper/visuals/display/ItemDisplayManager.kt new file mode 100644 index 0000000..c3f527f --- /dev/null +++ b/src/main/kotlin/cc/modlabs/kpaper/visuals/display/ItemDisplayManager.kt @@ -0,0 +1,582 @@ +package cc.modlabs.kpaper.visuals.display + +import com.github.retrooper.packetevents.PacketEvents +import com.github.retrooper.packetevents.protocol.entity.data.EntityData +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes +import com.github.retrooper.packetevents.protocol.item.ItemStack as PacketItemStack +import com.github.retrooper.packetevents.protocol.item.type.ItemType +import com.github.retrooper.packetevents.protocol.item.type.ItemTypes +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerDestroyEntities +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityMetadata +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSpawnEntity +import com.github.retrooper.packetevents.util.Vector3d +import com.github.retrooper.packetevents.util.Vector3f +import org.bukkit.Location +import org.bukkit.Material +import org.bukkit.entity.Player +import org.bukkit.inventory.ItemStack +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicInteger + +/** + * A manager class for creating and managing packet-based Item Display entities in Minecraft. + * + * Item Displays are client-side entities that can display items at specific locations. + * This manager handles the lifecycle of these displays, including creation, updates, and removal. + * + * @example + * ```kotlin + * val manager = ItemDisplayManager() + * val display = manager.createItemDisplay( + * item = ItemStack(Material.DIAMOND_SWORD), + * location = player.location, + * viewer = player, + * billboard = ItemDisplayManager.ItemDisplayBillboard.CENTER, + * glow = true, + * scale = Vector3f(1.0f, 1.0f, 1.0f), + * displayTransformation = ItemDisplayManager.ItemDisplayTransformation.GROUND + * ) + * ``` + */ +class ItemDisplayManager { + + /** + * Stores all active Item Displays, mapped by Entity ID. + */ + private val activeDisplays = ConcurrentHashMap() + + /** + * Counter for generating unique Entity IDs. + * Starts at 2000000 to avoid conflicts with regular entities and Text Displays. + */ + private val nextEntityId = AtomicInteger(2000000) + + /** + * Creates an Item Display for a specific player. + * + * @param item The item to display + * @param location The position of the Item Display + * @param viewer The player who should see the Item Display + * @param billboard The billboard mode for the display + * @param glow Whether the display should glow (outline effect) + * @param scale The scale of the item (default: 0.5f, 0.5f, 0.5f) + * @param displayTransformation How the item should be displayed (default: NONE) + * @return The created ItemDisplay object + */ + fun createItemDisplay( + item: ItemStack, + location: Location, + viewer: Player, + billboard: ItemDisplayBillboard, + glow: Boolean = false, + scale: Vector3f = Vector3f(0.5f, 0.5f, 0.5f), + displayTransformation: ItemDisplayTransformation = ItemDisplayTransformation.NONE + ): ItemDisplay { + val entityId = nextEntityId.getAndIncrement() + val itemType = getItemType(item.type) + val itemDisplay = ItemDisplay( + entityId, item, location.clone(), mutableSetOf(viewer), billboard, glow, itemType, scale, displayTransformation + ) + + spawnItemDisplay(itemDisplay, viewer) + activeDisplays[entityId] = itemDisplay + + return itemDisplay + } + + /** + * Creates an Item Display for multiple players. + * + * @param item The item to display + * @param location The position of the Item Display + * @param viewers The players who should see the Item Display + * @param billboard The billboard mode for the display + * @param glow Whether the display should glow (outline effect) + * @param scale The scale of the item (default: 1.0f, 1.0f, 1.0f) + * @param displayTransformation How the item should be displayed (default: NONE) + * @return The created ItemDisplay object + */ + fun createItemDisplay( + item: ItemStack, + location: Location, + viewers: Collection, + billboard: ItemDisplayBillboard, + glow: Boolean = false, + scale: Vector3f = Vector3f(1.0f, 1.0f, 1.0f), + displayTransformation: ItemDisplayTransformation = ItemDisplayTransformation.NONE + ): ItemDisplay { + val entityId = nextEntityId.getAndIncrement() + val itemType = getItemType(item.type) + val itemDisplay = ItemDisplay( + entityId, item, location.clone(), viewers.toMutableSet(), billboard, glow, itemType, scale, displayTransformation + ) + + for (viewer in viewers) { + spawnItemDisplay(itemDisplay, viewer) + } + + activeDisplays[entityId] = itemDisplay + return itemDisplay + } + + /** + * Shows an existing Item Display to an additional player. + * + * @param itemDisplay The ItemDisplay to show + * @param viewer The player who should see the Item Display + * @return `true` if the viewer was added, `false` if they could already see the display + */ + fun addViewer(itemDisplay: ItemDisplay, viewer: Player): Boolean { + if (itemDisplay.viewers.add(viewer)) { + spawnItemDisplay(itemDisplay, viewer) + return true + } + return false + } + + /** + * Hides an Item Display for a specific player. + * + * @param itemDisplay The ItemDisplay to hide + * @param viewer The player for whom the display should be hidden + * @return `true` if the viewer was removed, `false` if they couldn't see the display + */ + fun removeViewer(itemDisplay: ItemDisplay, viewer: Player): Boolean { + if (itemDisplay.viewers.remove(viewer)) { + destroyEntity(viewer, itemDisplay.entityId) + + // If no viewers remain, remove the display completely + if (itemDisplay.viewers.isEmpty()) { + activeDisplays.remove(itemDisplay.entityId) + } + + return true + } + return false + } + + /** + * Updates the item of an existing Item Display. + * + * @param itemDisplay The ItemDisplay to update + * @param newItem The new item to display + */ + fun updateItem(itemDisplay: ItemDisplay, newItem: ItemStack) { + itemDisplay.item = newItem + itemDisplay.itemType = getItemType(newItem.type) + + // Send metadata update for all viewers + val metadataPacket = createMetadataPacket(itemDisplay) + for (viewer in itemDisplay.viewers) { + PacketEvents.getAPI().playerManager.sendPacket(viewer, metadataPacket) + } + } + + /** + * Updates the scale of an existing Item Display. + * + * @param itemDisplay The ItemDisplay to update + * @param newScale The new scale + */ + fun updateScale(itemDisplay: ItemDisplay, newScale: Vector3f) { + itemDisplay.scale = newScale + + val metadataPacket = createMetadataPacket(itemDisplay) + for (viewer in itemDisplay.viewers) { + PacketEvents.getAPI().playerManager.sendPacket(viewer, metadataPacket) + } + } + + /** + * Updates the transformation type of an existing Item Display. + * + * @param itemDisplay The ItemDisplay to update + * @param newTransformation The new transformation type + */ + fun updateTransformation(itemDisplay: ItemDisplay, newTransformation: ItemDisplayTransformation) { + itemDisplay.displayTransformation = newTransformation + + val metadataPacket = createMetadataPacket(itemDisplay) + for (viewer in itemDisplay.viewers) { + PacketEvents.getAPI().playerManager.sendPacket(viewer, metadataPacket) + } + } + + /** + * Updates the position of an existing Item Display. + * + * Note: This requires respawning the entity for all viewers, which may cause a brief flicker. + * + * @param itemDisplay The ItemDisplay to update + * @param newLocation The new position + */ + fun updateLocation(itemDisplay: ItemDisplay, newLocation: Location) { + itemDisplay.location = newLocation.clone() + + // For all viewers, respawn the display (there's no simple packet for position updates) + for (viewer in itemDisplay.viewers.toSet()) { // Create copy to avoid ConcurrentModification + destroyEntity(viewer, itemDisplay.entityId) + spawnItemDisplay(itemDisplay, viewer) + } + } + + /** + * Removes an Item Display completely. + * + * @param itemDisplay The ItemDisplay to remove + */ + fun removeItemDisplay(itemDisplay: ItemDisplay) { + activeDisplays.remove(itemDisplay.entityId) + + for (viewer in itemDisplay.viewers.toSet()) { + destroyEntity(viewer, itemDisplay.entityId) + } + + itemDisplay.viewers.clear() + } + + /** + * Removes all Item Displays. + */ + fun removeAllItemDisplays() { + for (itemDisplay in activeDisplays.values.toList()) { + removeItemDisplay(itemDisplay) + } + } + + /** + * Gets an ItemDisplay by its entity ID. + * + * @param entityId The entity ID of the ItemDisplay + * @return The ItemDisplay or `null` if it doesn't exist + */ + operator fun get(entityId: Int): ItemDisplay? { + return activeDisplays[entityId] + } + + /** + * Gets all active ItemDisplays. + * + * @return A collection of all active ItemDisplays + */ + fun getAllItemDisplays(): Collection { + return activeDisplays.values + } + + /** + * Checks if a specific entity ID belongs to an ItemDisplay. + * + * @param entityId The entity ID to check + * @return `true` if an ItemDisplay with this ID exists + */ + fun isItemDisplay(entityId: Int): Boolean { + return activeDisplays.containsKey(entityId) + } + + /** + * Converts a Bukkit Material to a PacketEvents ItemType. + * + * @param material The Bukkit Material + * @return The corresponding PacketEvents ItemType + */ + private fun getItemType(material: Material): ItemType = ItemTypes.getByName(material.name) ?: ItemTypes.AIR + + /** + * Converts a Bukkit ItemStack to a PacketEvents ItemStack. + * + * @param bukkitItem The Bukkit ItemStack + * @return The PacketEvents ItemStack + */ + private fun toPacketItemStack(bukkitItem: ItemStack): PacketItemStack { + val itemType = getItemType(bukkitItem.type) + return PacketItemStack.builder() + .type(itemType) + .amount(bukkitItem.amount) + .build() + } + + /** + * Sends the packets to spawn an Item Display for a player. + * + * @param itemDisplay The ItemDisplay to spawn + * @param viewer The player who should see the display + */ + private fun spawnItemDisplay(itemDisplay: ItemDisplay, viewer: Player) { + try { + // 1. Spawn Entity Packet + val location = itemDisplay.location + val spawnPacket = WrapperPlayServerSpawnEntity( + itemDisplay.entityId, + Optional.of(UUID.randomUUID()), + EntityTypes.ITEM_DISPLAY, + Vector3d(location.x, location.y + itemDisplay.yOffset, location.z), + location.pitch, + location.yaw, + location.yaw, + 0, + Optional.of(Vector3d(0.0, 0.0, 0.0)) + ) + + // 2. Entity Metadata Packet + val metadataPacket = createMetadataPacket(itemDisplay) + + // Send packets + PacketEvents.getAPI().playerManager.sendPacket(viewer, spawnPacket) + PacketEvents.getAPI().playerManager.sendPacket(viewer, metadataPacket) + } catch (e: Exception) { + // In case of an error, log it instead of crashing the plugin + e.printStackTrace() + } + } + + /** + * Creates a metadata packet for an Item Display. + * + * @param itemDisplay The ItemDisplay + * @return The created EntityMetadata packet + */ + private fun createMetadataPacket(itemDisplay: ItemDisplay): WrapperPlayServerEntityMetadata { + val metadataList = ArrayList>() + + // Billboard mode (index 15) + metadataList.add(EntityData(15, EntityDataTypes.BYTE, itemDisplay.billboard.billboardValue.toByte())) + + // Glow effect (index 22) + metadataList.add(EntityData(22, EntityDataTypes.INT, itemDisplay.glowingInt())) + + // The item itself (index 23) + val packetItem = toPacketItemStack(itemDisplay.item) + metadataList.add(EntityData(23, EntityDataTypes.ITEMSTACK, packetItem)) + + // Display transformation (index 24) - how the item is displayed + metadataList.add(EntityData(24, EntityDataTypes.BYTE, itemDisplay.displayTransformation.value.toByte())) + + // Scale of the item (index 12) + metadataList.add(EntityData(12, EntityDataTypes.VECTOR3F, itemDisplay.scale)) + + return WrapperPlayServerEntityMetadata(itemDisplay.entityId, metadataList) + } + + /** + * Sends a Destroy-Entity packet to a player. + * + * @param player The player + * @param entityId The entity ID to destroy + */ + internal fun destroyEntity(player: Player, entityId: Int) { + val destroyPacket = WrapperPlayServerDestroyEntities(entityId) + PacketEvents.getAPI().playerManager.sendPacket(player, destroyPacket) + } + + /** + * Billboard modes for Item Displays. + * Controls how the item display rotates relative to the viewer. + */ + enum class ItemDisplayBillboard(val billboardValue: Int) { + /** + * Fixed orientation - doesn't rotate, always faces the same direction. + */ + FIXED(0), + + /** + * Vertical rotation - rotates around Y axis to face the player horizontally. + */ + VERTICAL(1), + + /** + * Horizontal rotation - rotates around X axis to face the player vertically. + */ + HORIZONTAL(2), + + /** + * Center rotation - rotates to fully face the player (both axes). + */ + CENTER(3) + } + + /** + * Display transformation modes for Item Displays. + * Controls how the item is rendered (perspective, position, etc.). + */ + enum class ItemDisplayTransformation(val value: Int) { + /** + * No transformation - default display. + */ + NONE(0), + + /** + * Third person left hand perspective. + */ + THIRDPERSON_LEFTHAND(1), + + /** + * Third person right hand perspective. + */ + THIRDPERSON_RIGHTHAND(2), + + /** + * First person left hand perspective. + */ + FIRSTPERSON_LEFTHAND(3), + + /** + * First person right hand perspective. + */ + FIRSTPERSON_RIGHTHAND(4), + + /** + * Head/helmet perspective. + */ + HEAD(5), + + /** + * GUI/inventory perspective. + */ + GUI(6), + + /** + * Ground/dropped item perspective. + */ + GROUND(7), + + /** + * Fixed perspective. + */ + FIXED(8) + } + + /** + * Represents an Item Display entity. + * + * @property entityId The unique entity ID assigned to this display + * @property item The item being displayed + * @property location The current location of the display + * @property viewers The set of players who can currently see this display + * @property billboard The billboard mode for rotation behavior + * @property glowing Whether the display has a glow effect + * @property itemType The PacketEvents ItemType (internal use) + * @property scale The scale of the item display + * @property displayTransformation How the item is displayed + */ + class ItemDisplay( + val entityId: Int, + var item: ItemStack, + var location: Location, + val viewers: MutableSet, + val billboard: ItemDisplayBillboard, + val glowing: Boolean = false, + var itemType: ItemType, + var scale: Vector3f = Vector3f(1.0f, 1.0f, 1.0f), + var displayTransformation: ItemDisplayTransformation = ItemDisplayTransformation.NONE + ) { + /** + * Vertical offset from the base location. + * How far above the ground/position the display should appear. + */ + var yOffset: Double = 0.0 + + /** + * Sets the item of the display. + * Shortcut for [ItemDisplayManager.updateItem]. + * + * @param manager The manager instance to use for the update + * @param newItem The new item to display + */ + fun setItem(manager: ItemDisplayManager, newItem: ItemStack) { + manager.updateItem(this, newItem) + } + + /** + * Gets the glow effect value for the entity metadata. + * + * @return 10 if glowing, -1 otherwise + */ + fun glowingInt(): Int { + return if (glowing) { + 10 + } else { + -1 + } + } + + /** + * Changes the scale of the display. + * Shortcut for [ItemDisplayManager.updateScale]. + * + * @param manager The manager instance to use for the update + * @param newScale The new scale + */ + fun setScale(manager: ItemDisplayManager, newScale: Vector3f) { + manager.updateScale(this, newScale) + } + + /** + * Changes the transformation type of the display. + * Shortcut for [ItemDisplayManager.updateTransformation]. + * + * @param manager The manager instance to use for the update + * @param newTransformation The new transformation type + */ + fun setTransformation(manager: ItemDisplayManager, newTransformation: ItemDisplayTransformation) { + manager.updateTransformation(this, newTransformation) + } + + /** + * Moves the display to a new position. + * Shortcut for [ItemDisplayManager.updateLocation]. + * + * @param manager The manager instance to use for the update + * @param newLocation The new position + */ + fun moveToLocation(manager: ItemDisplayManager, newLocation: Location) { + manager.updateLocation(this, newLocation) + } + + /** + * Adds a new viewer. + * Shortcut for [ItemDisplayManager.addViewer]. + * + * @param manager The manager instance to use + * @param viewer The player to add as a viewer + * @return `true` if the viewer was added, `false` if they could already see it + */ + fun addViewer(manager: ItemDisplayManager, viewer: Player): Boolean { + return manager.addViewer(this, viewer) + } + + /** + * Removes a viewer. + * Shortcut for [ItemDisplayManager.removeViewer]. + * + * @param manager The manager instance to use + * @param viewer The player to remove as a viewer + * @return `true` if the viewer was removed, `false` if they couldn't see it + */ + fun removeViewer(manager: ItemDisplayManager, viewer: Player): Boolean { + return manager.removeViewer(this, viewer) + } + + /** + * Removes this display. + * Shortcut for [ItemDisplayManager.removeItemDisplay]. + * + * @param manager The manager instance to use + */ + fun remove(manager: ItemDisplayManager) { + manager.removeItemDisplay(this) + } + + /** + * Checks if the display is visible to a specific player. + * + * @param player The player to check + * @return `true` if the player can see the display + */ + fun isVisibleTo(player: Player): Boolean { + return player in viewers + } + } +} + diff --git a/src/main/kotlin/cc/modlabs/kpaper/visuals/display/TextDisplayFlags.kt b/src/main/kotlin/cc/modlabs/kpaper/visuals/display/TextDisplayFlags.kt new file mode 100644 index 0000000..a87d82b --- /dev/null +++ b/src/main/kotlin/cc/modlabs/kpaper/visuals/display/TextDisplayFlags.kt @@ -0,0 +1,92 @@ +package cc.modlabs.kpaper.visuals.display + +/** + * Flags that can be applied to a Text Display entity. + * These flags control various display behaviors such as shadow, transparency, background, and text alignment. + */ +enum class TextDisplayFlags(val bitValue: Int) { + /** + * Shows the text with a shadow effect. + */ + HAS_SHADOW(0x01), + + /** + * Makes the text see-through (transparent). + */ + IS_SEE_THROUGH(0x02), + + /** + * Uses the default background color instead of a custom one. + */ + USE_DEFAULT_BACKGROUND_COLOR(0x04), + + /** + * Centers the text alignment. + */ + ALIGNMENT_CENTER(0x00), + + /** + * Aligns the text to the left. + */ + ALIGNMENT_LEFT(0x08), + + /** + * Aligns the text to the right. + */ + ALIGNMENT_RIGHT(0x10); + + companion object { + /** + * Calculates a bitmask from a list of flags. + * Only one alignment flag can be used at a time. + * + * @param flags The list of flags to combine + * @return The combined bitmask value + * @throws IllegalArgumentException if multiple alignment flags are provided + */ + fun calculateBitMask(flags: List): Int { + var mask = 0 + + // Only one alignment may be selected + val alignmentFlags = flags.filter { + it == ALIGNMENT_CENTER || it == ALIGNMENT_LEFT || it == ALIGNMENT_RIGHT + } + + if (alignmentFlags.size > 1) { + throw IllegalArgumentException("Nur eine Ausrichtung kann verwendet werden") + } + + // Add all flags to the mask + for (flag in flags) { + mask = mask or flag.bitValue + } + + return mask + } + + /** + * Creates a flag list from a bitmask. + * + * @param mask The bitmask to decode + * @return A list of flags represented by the bitmask + */ + fun fromBitMask(mask: Int): List { + val result = mutableListOf() + + // Check other flags + if (mask and HAS_SHADOW.bitValue != 0) result.add(HAS_SHADOW) + if (mask and IS_SEE_THROUGH.bitValue != 0) result.add(IS_SEE_THROUGH) + if (mask and USE_DEFAULT_BACKGROUND_COLOR.bitValue != 0) result.add(USE_DEFAULT_BACKGROUND_COLOR) + + // Determine the alignment + when { + mask and ALIGNMENT_LEFT.bitValue != 0 -> result.add(ALIGNMENT_LEFT) + mask and ALIGNMENT_RIGHT.bitValue != 0 -> result.add(ALIGNMENT_RIGHT) + else -> result.add(ALIGNMENT_CENTER) // Default is CENTER + } + + return result + } + } +} + diff --git a/src/main/kotlin/cc/modlabs/kpaper/visuals/display/TextDisplayManager.kt b/src/main/kotlin/cc/modlabs/kpaper/visuals/display/TextDisplayManager.kt new file mode 100644 index 0000000..aebb8c0 --- /dev/null +++ b/src/main/kotlin/cc/modlabs/kpaper/visuals/display/TextDisplayManager.kt @@ -0,0 +1,468 @@ +package cc.modlabs.kpaper.visuals.display + +import com.github.retrooper.packetevents.PacketEvents +import com.github.retrooper.packetevents.protocol.entity.data.EntityData +import com.github.retrooper.packetevents.protocol.entity.data.EntityDataTypes +import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerDestroyEntities +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerEntityMetadata +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerSpawnEntity +import com.github.retrooper.packetevents.util.Vector3d +import dev.fruxz.stacked.text +import org.bukkit.Location +import org.bukkit.entity.Player +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicInteger + +/** + * A manager class for creating and managing packet-based Text Display entities in Minecraft. + * + * Text Displays are client-side entities that can display formatted text at specific locations. + * This manager handles the lifecycle of these displays, including creation, updates, and removal. + * + * @example + * ```kotlin + * val manager = TextDisplayManager() + * val display = manager.createTextDisplay( + * text = "Hello World!", + * location = player.location, + * viewer = player, + * billboard = TextDisplayManager.TextDisplayBillboard.CENTER, + * glow = false, + * opacity = 255, + * displayFlags = listOf(TextDisplayFlags.HAS_SHADOW), + * backgroundColor = 0x40000000 + * ) + * ``` + */ +class TextDisplayManager { + + /** + * Stores all active Text Displays, mapped by Entity ID. + */ + private val activeDisplays = ConcurrentHashMap() + + /** + * Counter for generating unique Entity IDs. + * Starts at 1000000 to avoid conflicts with regular entities. + */ + private val nextEntityId = AtomicInteger(1000000) + + /** + * Creates a Text Display for a specific player. + * + * @param text The text to display (supports color codes and Adventure components) + * @param location The position of the Text Display + * @param viewer The player who should see the Text Display + * @param billboard The billboard mode for the display + * @param glow Whether the display should glow (outline effect) + * @param opacity The opacity of the text (0-255, -1 for default) + * @param displayFlags The display flags to apply (shadow, transparency, alignment, etc.) + * @param backgroundColor The background color in ARGB format (0xAARRGGBB) + * @return The created TextDisplay object + */ + fun createTextDisplay( + text: String, + location: Location, + viewer: Player, + billboard: TextDisplayBillboard, + glow: Boolean = false, + opacity: Int, + displayFlags: List, + backgroundColor: Int, + ): TextDisplay { + val entityId = nextEntityId.getAndIncrement() + val textDisplay = TextDisplay( + entityId, text, location.clone(), mutableSetOf(viewer), billboard, glow, opacity, displayFlags, backgroundColor + ) + + spawnTextDisplay(textDisplay, viewer) + activeDisplays[entityId] = textDisplay + + return textDisplay + } + + /** + * Creates a Text Display for multiple players. + * + * @param text The text to display (supports color codes and Adventure components) + * @param location The position of the Text Display + * @param viewers The players who should see the Text Display + * @param billboard The billboard mode for the display + * @param glow Whether the display should glow (outline effect) + * @param opacity The opacity of the text (0-255, -1 for default) + * @param displayFlags The display flags to apply (shadow, transparency, alignment, etc.) + * @param backgroundColor The background color in ARGB format (0xAARRGGBB) + * @return The created TextDisplay object + */ + fun createTextDisplay( + text: String, + location: Location, + viewers: Collection, + billboard: TextDisplayBillboard, + glow: Boolean = false, + opacity: Int, + displayFlags: List, + backgroundColor: Int, + ): TextDisplay { + val entityId = nextEntityId.getAndIncrement() + val textDisplay = TextDisplay( + entityId, text, location.clone(), viewers.toMutableSet(), billboard, glow, opacity, displayFlags, backgroundColor + ) + + for (viewer in viewers) { + spawnTextDisplay(textDisplay, viewer) + } + + activeDisplays[entityId] = textDisplay + return textDisplay + } + + /** + * Shows an existing Text Display to an additional player. + * + * @param textDisplay The TextDisplay to show + * @param viewer The player who should see the Text Display + * @return `true` if the viewer was added, `false` if they could already see the display + */ + fun addViewer(textDisplay: TextDisplay, viewer: Player): Boolean { + if (textDisplay.viewers.add(viewer)) { + spawnTextDisplay(textDisplay, viewer) + return true + } + return false + } + + /** + * Hides a Text Display for a specific player. + * + * @param textDisplay The TextDisplay to hide + * @param viewer The player for whom the display should be hidden + * @return `true` if the viewer was removed, `false` if they couldn't see the display + */ + fun removeViewer(textDisplay: TextDisplay, viewer: Player): Boolean { + if (textDisplay.viewers.remove(viewer)) { + destroyEntity(viewer, textDisplay.entityId) + + // If no viewers remain, remove the display completely + if (textDisplay.viewers.isEmpty()) { + activeDisplays.remove(textDisplay.entityId) + } + + return true + } + return false + } + + /** + * Updates the text of an existing Text Display. + * + * @param textDisplay The TextDisplay to update + * @param newText The new text to display + */ + fun updateText(textDisplay: TextDisplay, newText: String) { + textDisplay.text = newText + + // Send metadata update for all viewers + val metadataPacket = createMetadataPacket(textDisplay) + for (viewer in textDisplay.viewers) { + PacketEvents.getAPI().playerManager.sendPacket(viewer, metadataPacket) + } + } + + /** + * Updates the position of an existing Text Display. + * + * Note: This requires respawning the entity for all viewers, which may cause a brief flicker. + * + * @param textDisplay The TextDisplay to update + * @param newLocation The new position + */ + fun updateLocation(textDisplay: TextDisplay, newLocation: Location) { + textDisplay.location = newLocation.clone() + + // For all viewers, respawn the display (there's no simple packet for position updates) + for (viewer in textDisplay.viewers.toSet()) { // Create copy to avoid ConcurrentModification + destroyEntity(viewer, textDisplay.entityId) + spawnTextDisplay(textDisplay, viewer) + } + } + + /** + * Removes a Text Display completely. + * + * @param textDisplay The TextDisplay to remove + */ + fun removeTextDisplay(textDisplay: TextDisplay) { + activeDisplays.remove(textDisplay.entityId) + + for (viewer in textDisplay.viewers.toSet()) { + destroyEntity(viewer, textDisplay.entityId) + } + + textDisplay.viewers.clear() + } + + /** + * Removes all Text Displays. + */ + fun removeAllTextDisplays() { + for (textDisplay in activeDisplays.values.toList()) { + removeTextDisplay(textDisplay) + } + } + + /** + * Gets a TextDisplay by its entity ID. + * + * @param entityId The entity ID of the TextDisplay + * @return The TextDisplay or `null` if it doesn't exist + */ + operator fun get(entityId: Int): TextDisplay? { + return activeDisplays[entityId] + } + + /** + * Gets all active TextDisplays. + * + * @return A collection of all active TextDisplays + */ + fun getAllTextDisplays(): Collection { + return activeDisplays.values + } + + /** + * Checks if a specific entity ID belongs to a TextDisplay. + * + * @param entityId The entity ID to check + * @return `true` if a TextDisplay with this ID exists + */ + fun isTextDisplay(entityId: Int): Boolean { + return activeDisplays.containsKey(entityId) + } + + /** + * Sends the packets to spawn a Text Display for a player. + * + * @param textDisplay The TextDisplay to spawn + * @param viewer The player who should see the display + */ + private fun spawnTextDisplay(textDisplay: TextDisplay, viewer: Player) { + try { + // 1. Spawn Entity Packet + val location = textDisplay.location + val spawnPacket = WrapperPlayServerSpawnEntity( + textDisplay.entityId, + Optional.of(UUID.randomUUID()), + EntityTypes.TEXT_DISPLAY, + Vector3d(location.x, location.y + textDisplay.yOffset, location.z), + location.pitch.toFloat(), + location.yaw.toFloat(), + location.yaw.toFloat(), + 0, + Optional.of(Vector3d(0.0, 0.0, 0.0)) + ) + + // 2. Entity Metadata Packet + val metadataPacket = createMetadataPacket(textDisplay) + + // Send packets + PacketEvents.getAPI().playerManager.sendPacket(viewer, spawnPacket) + PacketEvents.getAPI().playerManager.sendPacket(viewer, metadataPacket) + } catch (e: Exception) { + // In case of an error, log it instead of crashing the plugin + e.printStackTrace() + } + } + + /** + * Creates a metadata packet for a Text Display. + * + * @param textDisplay The TextDisplay + * @return The created EntityMetadata packet + */ + private fun createMetadataPacket(textDisplay: TextDisplay): WrapperPlayServerEntityMetadata { + val metadataList = ArrayList>() + + // Billboard mode (index 15) + metadataList.add(EntityData(15, EntityDataTypes.BYTE, textDisplay.billboard.billboardValue.toByte())) + + // Glow effect (index 22) + metadataList.add(EntityData(22, EntityDataTypes.INT, textDisplay.glowingInt())) + + // Text content (index 23) + metadataList.add(EntityData(23, EntityDataTypes.ADV_COMPONENT, text(textDisplay.text))) + + // Text width (index 24) + metadataList.add(EntityData(24, EntityDataTypes.INT, textDisplay.width.toInt())) + + // Background color (ARGB) (index 25) + metadataList.add(EntityData(25, EntityDataTypes.INT, textDisplay.backgroundColor)) + + // Opacity (index 26) + metadataList.add(EntityData(26, EntityDataTypes.BYTE, textDisplay.opacity.toByte())) + + // Display flags (index 27) + metadataList.add(EntityData(27, EntityDataTypes.BYTE, TextDisplayFlags.calculateBitMask(textDisplay.displayFlags).toByte())) + + return WrapperPlayServerEntityMetadata(textDisplay.entityId, metadataList) + } + + /** + * Sends a Destroy-Entity packet to a player. + * + * @param player The player + * @param entityId The entity ID to destroy + */ + internal fun destroyEntity(player: Player, entityId: Int) { + // Create packet with the entity ID + val destroyPacket = WrapperPlayServerDestroyEntities(entityId) + + // Send packet to player + PacketEvents.getAPI().playerManager.sendPacket(player, destroyPacket) + } + + /** + * Billboard modes for Text Displays. + * Controls how the text display rotates relative to the viewer. + */ + enum class TextDisplayBillboard(val billboardValue: Int) { + /** + * Fixed orientation - doesn't rotate, always faces the same direction. + */ + FIXED(0), + + /** + * Vertical rotation - rotates around Y axis to face the player horizontally. + */ + VERTICAL(1), + + /** + * Horizontal rotation - rotates around X axis to face the player vertically. + */ + HORIZONTAL(2), + + /** + * Center rotation - rotates to fully face the player (both axes). + */ + CENTER(3) + } + + /** + * Represents a Text Display entity. + * + * @property entityId The unique entity ID assigned to this display + * @property text The text content to display + * @property location The current location of the display + * @property viewers The set of players who can currently see this display + * @property billboard The billboard mode for rotation behavior + * @property glowing Whether the display has a glow effect + * @property opacity The opacity of the text (0-255, -1 for default) + * @property displayFlags The display flags applied to this display + * @property backgroundColor The background color in ARGB format + */ + class TextDisplay( + val entityId: Int, + var text: String, + var location: Location, + val viewers: MutableSet, + val billboard: TextDisplayBillboard, + val glowing: Boolean = false, + val opacity: Int = -1, + val displayFlags: List, + val backgroundColor: Int = 0x00000000 + ) { + /** + * Vertical offset from the base location. + * How far above the ground/position the display should appear. + */ + var yOffset: Double = 0.0 + + /** + * Maximum text width in pixels. + * Text will wrap if it exceeds this width. + */ + var width: Float = 200f + + /** + * Sets the text of the display. + * Shortcut for [TextDisplayManager.updateText]. + * + * @param manager The manager instance to use for the update + * @param newText The new text to display + */ + fun setText(manager: TextDisplayManager, newText: String) { + manager.updateText(this, newText) + } + + /** + * Gets the glow effect value for the entity metadata. + * + * @return 10 if glowing, -1 otherwise + */ + fun glowingInt(): Int { + return if (glowing) { + 10 + } else { + -1 + } + } + + /** + * Moves the display to a new position. + * Shortcut for [TextDisplayManager.updateLocation]. + * + * @param manager The manager instance to use for the update + * @param newLocation The new position + */ + fun moveToLocation(manager: TextDisplayManager, newLocation: Location) { + manager.updateLocation(this, newLocation) + } + + /** + * Adds a new viewer. + * Shortcut for [TextDisplayManager.addViewer]. + * + * @param manager The manager instance to use + * @param viewer The player to add as a viewer + * @return `true` if the viewer was added, `false` if they could already see it + */ + fun addViewer(manager: TextDisplayManager, viewer: Player): Boolean { + return manager.addViewer(this, viewer) + } + + /** + * Removes a viewer. + * Shortcut for [TextDisplayManager.removeViewer]. + * + * @param manager The manager instance to use + * @param viewer The player to remove as a viewer + * @return `true` if the viewer was removed, `false` if they couldn't see it + */ + fun removeViewer(manager: TextDisplayManager, viewer: Player): Boolean { + return manager.removeViewer(this, viewer) + } + + /** + * Removes this display. + * Shortcut for [TextDisplayManager.removeTextDisplay]. + * + * @param manager The manager instance to use + */ + fun remove(manager: TextDisplayManager) { + manager.removeTextDisplay(this) + } + + /** + * Checks if the display is visible to a specific player. + * + * @param player The player to check + * @return `true` if the player can see the display + */ + fun isVisibleTo(player: Player): Boolean { + return player in viewers + } + } +} +