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 package ch.offbeatwit.bestbuds
import ch.offbeatwit.bestbuds.managers.Context import ch.offbeatwit.bestbuds.managers.Context
import ch.offbeatwit.bestbuds.modules.AdoptionModule
import ch.offbeatwit.bestbuds.modules.AntiGriefModule import ch.offbeatwit.bestbuds.modules.AntiGriefModule
import ch.offbeatwit.bestbuds.modules.CommandsModule import ch.offbeatwit.bestbuds.modules.CommandsModule
import ch.offbeatwit.bestbuds.modules.InteractionModule import ch.offbeatwit.bestbuds.modules.InteractionModule
@ -16,6 +17,7 @@ class BestBudsPlugin : JavaPlugin() {
moduleLoader.load(::InteractionModule) moduleLoader.load(::InteractionModule)
moduleLoader.load(::CommandsModule) moduleLoader.load(::CommandsModule)
moduleLoader.load(::AntiGriefModule) moduleLoader.load(::AntiGriefModule)
moduleLoader.load(::AdoptionModule)
} }
override fun onDisable() { 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") val root = literal("bestbuds")
.then(transfer()) .then(transfer())
.then(release()) .then(release())
.then(allow()) .then(trust())
.then(disallow()) .then(untrust())
.executes(BestBudsHelpTopic) .executes(BestBudsHelpTopic)
.build() .build()
@ -78,8 +78,8 @@ class CommandsModule(ctx: Context) : BestBudsModule(ctx), CommandRegistrar {
} }
} }
fun allow(): CommandNode<CommandSourceStack> { fun trust(): CommandNode<CommandSourceStack> {
return literal("allow") return literal("trust")
.then(argument("player", ArgumentTypes.playerProfiles()) .then(argument("player", ArgumentTypes.playerProfiles())
.executes { ctx -> .executes { ctx ->
val sender = ctx.source.sender val sender = ctx.source.sender
@ -95,7 +95,7 @@ class CommandsModule(ctx: Context) : BestBudsModule(ctx), CommandRegistrar {
if (this.ctx.whitelistManager.addToWhitelist(sender, target)) { if (this.ctx.whitelistManager.addToWhitelist(sender, target)) {
sender.sendMessage(tr("${target.name} can now interact with your pets")) sender.sendMessage(tr("${target.name} can now interact with your pets"))
} else { } else {
sender.sendMessage(error("${target.name} is already on the allowlist")) sender.sendMessage(error("${target.name} is already trusted"))
} }
Command.SINGLE_SUCCESS Command.SINGLE_SUCCESS
@ -103,8 +103,8 @@ class CommandsModule(ctx: Context) : BestBudsModule(ctx), CommandRegistrar {
.build() .build()
} }
fun disallow(): CommandNode<CommandSourceStack> { fun untrust(): CommandNode<CommandSourceStack> {
return literal("disallow") return literal("untrust")
.then(argument("player", ArgumentTypes.playerProfiles()) .then(argument("player", ArgumentTypes.playerProfiles())
.executes { ctx -> .executes { ctx ->
val sender = ctx.source.sender val sender = ctx.source.sender
@ -120,7 +120,7 @@ class CommandsModule(ctx: Context) : BestBudsModule(ctx), CommandRegistrar {
if (this.ctx.whitelistManager.removeFromWhitelist(sender, target)) { if (this.ctx.whitelistManager.removeFromWhitelist(sender, target)) {
sender.sendMessage(tr("${target.name} can no longer interact with your pets")) sender.sendMessage(tr("${target.name} can no longer interact with your pets"))
} else { } else {
sender.sendMessage(error("${target.name} isn't on the allowlist")) sender.sendMessage(error("${target.name} isn't trusted"))
} }
Command.SINGLE_SUCCESS Command.SINGLE_SUCCESS

View file

@ -38,8 +38,8 @@ object BestBudsHelpTopic : HelpTopic(), Command<CommandSourceStack> {
val commands = listOf( val commands = listOf(
CommandHelp("/bb transfer <player>", "Change the owner of a pet to another player"), 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 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 trust <player>", "Allow another player to ride, leash, hurt and rename your pets"),
CommandHelp("/bb disallow <player>", "Remove a player from your allowlist"), CommandHelp("/bb untrust <player>", "Remove a player from your trusted list"),
) )
return Component.join(JoinConfiguration.newlines(), commands.map { 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.Player
import org.bukkit.entity.Tameable import org.bukkit.entity.Tameable
import org.bukkit.event.EventHandler import org.bukkit.event.EventHandler
import org.bukkit.event.EventPriority
import org.bukkit.event.Listener import org.bukkit.event.Listener
import org.bukkit.event.player.PlayerInteractEntityEvent import org.bukkit.event.player.PlayerInteractEntityEvent
import org.bukkit.util.Transformation import org.bukkit.util.Transformation
@ -23,8 +24,9 @@ import org.joml.Vector3f
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
class InteractionModule(ctx: Context) : BestBudsModule(ctx), Listener { class InteractionModule(ctx: Context) : BestBudsModule(ctx), Listener {
@EventHandler @EventHandler(priority = EventPriority.HIGH)
fun onClickPet(ev: PlayerInteractEntityEvent) { fun onClickPet(ev: PlayerInteractEntityEvent) {
if (ev.isCancelled) return
if (!ev.player.isSneaking) return if (!ev.player.isSneaking) return
val target = ev.rightClicked val target = ev.rightClicked
@ -54,11 +56,13 @@ class InteractionModule(ctx: Context) : BestBudsModule(ctx), Listener {
?.also { it.completeFromCache() } ?.also { it.completeFromCache() }
?.let { text(it.name ?: "unknown", NamedTextColor.GOLD) } ?.let { text(it.name ?: "unknown", NamedTextColor.GOLD) }
?: text("nobody", NamedTextColor.RED) ?: text("nobody", NamedTextColor.RED)
val component = Component.join( val component = Component.join(
JoinConfiguration.newlines(), JoinConfiguration.newlines(),
info.nameComponent, info.nameComponent,
text("Owned by ").append(owner), 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) ).style(INFO_TEXT_STYLE)
ctx.hologramManager.showSingle(this, pet.location, component) { ctx.hologramManager.showSingle(this, pet.location, component) {

View file

@ -18,10 +18,15 @@ class InfoDialog(val pet: Tameable) : Supplier<DialogLike> {
@Suppress("UnstableApiUsage") @Suppress("UnstableApiUsage")
override fun get(): DialogLike { override fun get(): DialogLike {
return Dialog.create { builder -> return Dialog.create { builder ->
val body = listOf( val body = mutableListOf(
DialogBody.plainMessage(text("Type: ").append(text(pet.type.name, NamedTextColor.GOLD))), DialogBody.plainMessage(text("Type: ").append(text(pet.type.name, NamedTextColor.GOLD))),
DialogBody.plainMessage(text("Health: ").append(info.healthComponent)), 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))) val base = DialogBase.builder(text("Managing ").append(pet.customName() ?: text(pet.name)))
.body(body) .body(body)
.canCloseWithEscape(true) .canCloseWithEscape(true)

View file

@ -1,11 +1,13 @@
package ch.offbeatwit.bestbuds.pet package ch.offbeatwit.bestbuds.pet
import ch.offbeatwit.bestbuds.IS_ADOPTABLE
import ch.offbeatwit.bestbuds.util.PET_NAME_STYLE import ch.offbeatwit.bestbuds.util.PET_NAME_STYLE
import net.kyori.adventure.text.Component import net.kyori.adventure.text.Component
import net.kyori.adventure.text.JoinConfiguration import net.kyori.adventure.text.JoinConfiguration
import net.kyori.adventure.text.format.NamedTextColor import net.kyori.adventure.text.format.NamedTextColor
import org.bukkit.attribute.Attribute import org.bukkit.attribute.Attribute
import org.bukkit.entity.Tameable import org.bukkit.entity.Tameable
import org.bukkit.persistence.PersistentDataType
class PetInfo(val pet: Tameable) { class PetInfo(val pet: Tameable) {
val nameComponent get() = (pet.customName() ?: Component.text(pet.name)).style(PET_NAME_STYLE) 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) 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)
}
}