diff --git a/src/main/kotlin/ch/offbeatwit/bestbuds/BestBudsPlugin.kt b/src/main/kotlin/ch/offbeatwit/bestbuds/BestBudsPlugin.kt index 2dd74dd..01a62c3 100644 --- a/src/main/kotlin/ch/offbeatwit/bestbuds/BestBudsPlugin.kt +++ b/src/main/kotlin/ch/offbeatwit/bestbuds/BestBudsPlugin.kt @@ -1,6 +1,7 @@ package ch.offbeatwit.bestbuds import ch.offbeatwit.bestbuds.managers.Context +import ch.offbeatwit.bestbuds.modules.AdoptionModule import ch.offbeatwit.bestbuds.modules.AntiGriefModule import ch.offbeatwit.bestbuds.modules.CommandsModule import ch.offbeatwit.bestbuds.modules.InteractionModule @@ -16,6 +17,7 @@ class BestBudsPlugin : JavaPlugin() { moduleLoader.load(::InteractionModule) moduleLoader.load(::CommandsModule) moduleLoader.load(::AntiGriefModule) + moduleLoader.load(::AdoptionModule) } override fun onDisable() { diff --git a/src/main/kotlin/ch/offbeatwit/bestbuds/DataKeys.kt b/src/main/kotlin/ch/offbeatwit/bestbuds/DataKeys.kt new file mode 100644 index 0000000..32708c9 --- /dev/null +++ b/src/main/kotlin/ch/offbeatwit/bestbuds/DataKeys.kt @@ -0,0 +1,6 @@ +package ch.offbeatwit.bestbuds + +import org.bukkit.NamespacedKey + +val ADOPTION_TARGET = NamespacedKey("bestbuds", "adoption_target") +val IS_ADOPTABLE = NamespacedKey("bestbuds", "is_adoptable") diff --git a/src/main/kotlin/ch/offbeatwit/bestbuds/modules/AdoptionModule.kt b/src/main/kotlin/ch/offbeatwit/bestbuds/modules/AdoptionModule.kt new file mode 100644 index 0000000..6b575e3 --- /dev/null +++ b/src/main/kotlin/ch/offbeatwit/bestbuds/modules/AdoptionModule.kt @@ -0,0 +1,106 @@ +package ch.offbeatwit.bestbuds.modules + +import ch.offbeatwit.bestbuds.ADOPTION_TARGET +import ch.offbeatwit.bestbuds.isOwnerOf +import ch.offbeatwit.bestbuds.managers.Context +import ch.offbeatwit.bestbuds.pet.PetInfo +import ch.offbeatwit.bestbuds.util.UUIDDataType +import ch.offbeatwit.bestbuds.util.error +import ch.offbeatwit.bestbuds.util.tr +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.Component.text +import net.kyori.adventure.text.JoinConfiguration +import org.bukkit.Material +import org.bukkit.entity.Player +import org.bukkit.entity.Tameable +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.player.PlayerInteractEntityEvent +import org.bukkit.inventory.EquipmentSlot +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.meta.BookMeta + +class AdoptionModule(ctx: Context) : BestBudsModule(ctx), Listener { + val adoptionItem = Material.PAPER + + @EventHandler + fun onInteract(ev: PlayerInteractEntityEvent) { + val target = ev.rightClicked + if (target !is Tameable || !target.isTamed) return + val info = PetInfo(target) + + val heldItem = ev.player.inventory.getItem(ev.hand) + + if (ev.player.isSneaking && ev.player.isOwnerOf(target) && heldItem.type == adoptionItem) { + ev.isCancelled = true + setupAdoption(ev.player, ev.hand, target) + } else if (heldItem.type == Material.WRITTEN_BOOK) { + val bookTarget = heldItem.persistentDataContainer[ADOPTION_TARGET, UUIDDataType] ?: return + ev.isCancelled = true + + if (target.uniqueId == bookTarget && info.isAdoptable) { + info.isAdoptable = false + heldItem.subtract() + + if (ev.player.isOwnerOf(target)) { + ev.player.sendActionBar(tr(info.nameComponent, text("is no longer adoptable"))) + } else { + target.owner = ev.player + + ev.player.sendActionBar( + tr( + text("Adopted ") + .append(info.nameComponent) + .append(text("! Take good care of them!")) + ) + ) + } + } else if (target.uniqueId == bookTarget) { + ev.player.sendActionBar(error("That pet is not currently adoptable")) + } else { + ev.player.sendActionBar(error("Those papers aren't for this pet!")) + } + } + } + + fun setupAdoption(sender: Player, hand: EquipmentSlot, pet: Tameable) { + val info = PetInfo(pet) + val stack = sender.inventory.getItem(hand) + if (stack.type != adoptionItem) return // sanity check + if (stack.amount == 0) return + + stack.subtract() + + val adoptionPaper = ItemStack(Material.WRITTEN_BOOK, 1).apply { + editMeta(BookMeta::class.java) { + it.title(Component.join(JoinConfiguration.spaces(), + text("Adoption papers for"), info.nameComponent)) + it.author(sender.displayName()) + + Component.join(JoinConfiguration.newlines(), + text("Adopt ").append(info.nameComponent).append(text(" today!")), + text(""), + text("Right-click on ").append(info.nameComponent) + .append(text(" with this book to adopt them.")) + ).apply { it.addPages(this) } + } + + editPersistentDataContainer { + it.set(ADOPTION_TARGET, UUIDDataType, pet.uniqueId) + } + } + + if (stack.isEmpty) { + sender.inventory.setItem(hand, adoptionPaper) + } else { + val res = sender.inventory.addItem(adoptionPaper) + if (res.isNotEmpty()) { + sender.world.dropItem(pet.location, adoptionPaper) + } + } + + info.isAdoptable = true + + sender.sendActionBar(tr("Set pet as adoptable!")) + } +} diff --git a/src/main/kotlin/ch/offbeatwit/bestbuds/modules/CommandsModule.kt b/src/main/kotlin/ch/offbeatwit/bestbuds/modules/CommandsModule.kt index 113b8b8..d8c8ddf 100644 --- a/src/main/kotlin/ch/offbeatwit/bestbuds/modules/CommandsModule.kt +++ b/src/main/kotlin/ch/offbeatwit/bestbuds/modules/CommandsModule.kt @@ -24,8 +24,8 @@ class CommandsModule(ctx: Context) : BestBudsModule(ctx), CommandRegistrar { val root = literal("bestbuds") .then(transfer()) .then(release()) - .then(allow()) - .then(disallow()) + .then(trust()) + .then(untrust()) .executes(BestBudsHelpTopic) .build() @@ -78,8 +78,8 @@ class CommandsModule(ctx: Context) : BestBudsModule(ctx), CommandRegistrar { } } - fun allow(): CommandNode { - return literal("allow") + fun trust(): CommandNode { + return literal("trust") .then(argument("player", ArgumentTypes.playerProfiles()) .executes { ctx -> val sender = ctx.source.sender @@ -95,7 +95,7 @@ class CommandsModule(ctx: Context) : BestBudsModule(ctx), CommandRegistrar { if (this.ctx.whitelistManager.addToWhitelist(sender, target)) { sender.sendMessage(tr("${target.name} can now interact with your pets")) } else { - sender.sendMessage(error("${target.name} is already on the allowlist")) + sender.sendMessage(error("${target.name} is already trusted")) } Command.SINGLE_SUCCESS @@ -103,8 +103,8 @@ class CommandsModule(ctx: Context) : BestBudsModule(ctx), CommandRegistrar { .build() } - fun disallow(): CommandNode { - return literal("disallow") + fun untrust(): CommandNode { + return literal("untrust") .then(argument("player", ArgumentTypes.playerProfiles()) .executes { ctx -> val sender = ctx.source.sender @@ -120,7 +120,7 @@ class CommandsModule(ctx: Context) : BestBudsModule(ctx), CommandRegistrar { if (this.ctx.whitelistManager.removeFromWhitelist(sender, target)) { sender.sendMessage(tr("${target.name} can no longer interact with your pets")) } else { - sender.sendMessage(error("${target.name} isn't on the allowlist")) + sender.sendMessage(error("${target.name} isn't trusted")) } Command.SINGLE_SUCCESS diff --git a/src/main/kotlin/ch/offbeatwit/bestbuds/modules/Help.kt b/src/main/kotlin/ch/offbeatwit/bestbuds/modules/Help.kt index 4138f40..aa78cdf 100644 --- a/src/main/kotlin/ch/offbeatwit/bestbuds/modules/Help.kt +++ b/src/main/kotlin/ch/offbeatwit/bestbuds/modules/Help.kt @@ -38,8 +38,8 @@ object BestBudsHelpTopic : HelpTopic(), Command { val commands = listOf( CommandHelp("/bb transfer ", "Change the owner of a pet to another player"), CommandHelp("/bb release", "Un-tame a pet, releasing them back into the wild"), - CommandHelp("/bb allow ", "Allow another player to ride, leash, hurt and rename your pets"), - CommandHelp("/bb disallow ", "Remove a player from your allowlist"), + CommandHelp("/bb trust ", "Allow another player to ride, leash, hurt and rename your pets"), + CommandHelp("/bb untrust ", "Remove a player from your trusted list"), ) return Component.join(JoinConfiguration.newlines(), commands.map { diff --git a/src/main/kotlin/ch/offbeatwit/bestbuds/modules/InteractionModule.kt b/src/main/kotlin/ch/offbeatwit/bestbuds/modules/InteractionModule.kt index 60ccb88..0895db5 100644 --- a/src/main/kotlin/ch/offbeatwit/bestbuds/modules/InteractionModule.kt +++ b/src/main/kotlin/ch/offbeatwit/bestbuds/modules/InteractionModule.kt @@ -15,6 +15,7 @@ import org.bukkit.entity.ChestedHorse import org.bukkit.entity.Player import org.bukkit.entity.Tameable import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority import org.bukkit.event.Listener import org.bukkit.event.player.PlayerInteractEntityEvent import org.bukkit.util.Transformation @@ -23,8 +24,9 @@ import org.joml.Vector3f import kotlin.time.Duration.Companion.seconds class InteractionModule(ctx: Context) : BestBudsModule(ctx), Listener { - @EventHandler + @EventHandler(priority = EventPriority.HIGH) fun onClickPet(ev: PlayerInteractEntityEvent) { + if (ev.isCancelled) return if (!ev.player.isSneaking) return val target = ev.rightClicked @@ -54,11 +56,13 @@ class InteractionModule(ctx: Context) : BestBudsModule(ctx), Listener { ?.also { it.completeFromCache() } ?.let { text(it.name ?: "unknown", NamedTextColor.GOLD) } ?: text("nobody", NamedTextColor.RED) + val component = Component.join( JoinConfiguration.newlines(), info.nameComponent, text("Owned by ").append(owner), - text("Health: ").append(info.healthComponent) + text("Health: ").append(info.healthComponent), + *(if (info.isAdoptable) arrayOf(text("Adoptable").color(NamedTextColor.GREEN)) else emptyArray()) ).style(INFO_TEXT_STYLE) ctx.hologramManager.showSingle(this, pet.location, component) { diff --git a/src/main/kotlin/ch/offbeatwit/bestbuds/pet/InfoDialog.kt b/src/main/kotlin/ch/offbeatwit/bestbuds/pet/InfoDialog.kt index ae4b72a..51c8b71 100644 --- a/src/main/kotlin/ch/offbeatwit/bestbuds/pet/InfoDialog.kt +++ b/src/main/kotlin/ch/offbeatwit/bestbuds/pet/InfoDialog.kt @@ -18,10 +18,15 @@ class InfoDialog(val pet: Tameable) : Supplier { @Suppress("UnstableApiUsage") override fun get(): DialogLike { return Dialog.create { builder -> - val body = listOf( + val body = mutableListOf( DialogBody.plainMessage(text("Type: ").append(text(pet.type.name, NamedTextColor.GOLD))), DialogBody.plainMessage(text("Health: ").append(info.healthComponent)), ) + + if (info.isAdoptable) { + body.add(DialogBody.plainMessage(text("Adoptable!", NamedTextColor.GREEN))) + } + val base = DialogBase.builder(text("Managing ").append(pet.customName() ?: text(pet.name))) .body(body) .canCloseWithEscape(true) diff --git a/src/main/kotlin/ch/offbeatwit/bestbuds/pet/PetInfo.kt b/src/main/kotlin/ch/offbeatwit/bestbuds/pet/PetInfo.kt index 46c9943..0fb9340 100644 --- a/src/main/kotlin/ch/offbeatwit/bestbuds/pet/PetInfo.kt +++ b/src/main/kotlin/ch/offbeatwit/bestbuds/pet/PetInfo.kt @@ -1,11 +1,13 @@ package ch.offbeatwit.bestbuds.pet +import ch.offbeatwit.bestbuds.IS_ADOPTABLE import ch.offbeatwit.bestbuds.util.PET_NAME_STYLE import net.kyori.adventure.text.Component import net.kyori.adventure.text.JoinConfiguration import net.kyori.adventure.text.format.NamedTextColor import org.bukkit.attribute.Attribute import org.bukkit.entity.Tameable +import org.bukkit.persistence.PersistentDataType class PetInfo(val pet: Tameable) { val nameComponent get() = (pet.customName() ?: Component.text(pet.name)).style(PET_NAME_STYLE) @@ -21,4 +23,8 @@ class PetInfo(val pet: Tameable) { Component.text(pet.health, healthColor), Component.text(maxHealth?.toString() ?: "???", healthColor) ) } + + var isAdoptable + get() = pet.persistentDataContainer[IS_ADOPTABLE, PersistentDataType.BOOLEAN] ?: false + set(value) = pet.persistentDataContainer.set(IS_ADOPTABLE, PersistentDataType.BOOLEAN, value) } diff --git a/src/main/kotlin/ch/offbeatwit/bestbuds/util/UUIDDataType.kt b/src/main/kotlin/ch/offbeatwit/bestbuds/util/UUIDDataType.kt new file mode 100644 index 0000000..914791b --- /dev/null +++ b/src/main/kotlin/ch/offbeatwit/bestbuds/util/UUIDDataType.kt @@ -0,0 +1,32 @@ +package ch.offbeatwit.bestbuds.util + +import org.bukkit.persistence.PersistentDataAdapterContext +import org.bukkit.persistence.PersistentDataType +import java.lang.Long +import java.nio.ByteBuffer +import java.util.* +import kotlin.ByteArray + +object UUIDDataType : PersistentDataType { + override fun getPrimitiveType(): Class { + return ByteArray::class.java + } + + override fun getComplexType(): Class { + return UUID::class.java + } + + override fun toPrimitive(complex: UUID, context: PersistentDataAdapterContext): ByteArray { + val bb = ByteBuffer.allocate(Long.BYTES * 2) + bb.putLong(complex.mostSignificantBits) + bb.putLong(complex.leastSignificantBits) + return bb.array() + } + + override fun fromPrimitive(primitive: ByteArray, context: PersistentDataAdapterContext): UUID { + val bb = ByteBuffer.wrap(primitive) + val msb = bb.getLong() + val lsb = bb.getLong() + return UUID(msb, lsb) + } +}