Compare commits

..

2 commits

Author SHA1 Message Date
2d9e35312b Add adoption module 2026-08-15 20:38:09 +01:00
20541c744e Rename allow/disallow commands to trust/untrust 2026-08-15 19:13:27 +01:00
9 changed files with 174 additions and 13 deletions

View file

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

View file

@ -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")

View file

@ -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!"))
}
}

View file

@ -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<CommandSourceStack> {
return literal("allow")
fun trust(): CommandNode<CommandSourceStack> {
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<CommandSourceStack> {
return literal("disallow")
fun untrust(): CommandNode<CommandSourceStack> {
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

View file

@ -38,8 +38,8 @@ object BestBudsHelpTopic : HelpTopic(), Command<CommandSourceStack> {
val commands = listOf(
CommandHelp("/bb transfer <player>", "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 <player>", "Allow another player to ride, leash, hurt and rename your pets"),
CommandHelp("/bb disallow <player>", "Remove a player from your allowlist"),
CommandHelp("/bb trust <player>", "Allow another player to ride, leash, hurt and rename your pets"),
CommandHelp("/bb untrust <player>", "Remove a player from your trusted list"),
)
return Component.join(JoinConfiguration.newlines(), commands.map {

View file

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

View file

@ -18,10 +18,15 @@ class InfoDialog(val pet: Tameable) : Supplier<DialogLike> {
@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)

View file

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

View file

@ -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<ByteArray, UUID> {
override fun getPrimitiveType(): Class<ByteArray> {
return ByteArray::class.java
}
override fun getComplexType(): Class<UUID> {
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)
}
}