From 0066481748e58edcc678a9b5d4f9aa7b48e5aaac Mon Sep 17 00:00:00 2001 From: Illyrius Date: Mon, 10 Nov 2025 14:14:56 +0100 Subject: [PATCH 01/24] init Signed-off-by: Illyrius --- .idea/dictionaries/project.xml | 1 + .../xodium/vanillaplus/modules/InvModule.kt | 30 +++++++------------ 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml index 1f044c595..8f06e7880 100644 --- a/.idea/dictionaries/project.xml +++ b/.idea/dictionaries/project.xml @@ -19,6 +19,7 @@ invs invsearch invu + invunload jpenilla ktlint mojang diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt index feb1f88a6..5ad3ff467 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt @@ -7,6 +7,7 @@ import com.mojang.brigadier.arguments.StringArgumentType import com.mojang.brigadier.context.CommandContext import io.papermc.paper.command.brigadier.CommandSourceStack import io.papermc.paper.command.brigadier.Commands +import io.papermc.paper.datacomponent.DataComponentTypes import net.kyori.adventure.sound.Sound import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder import org.bukkit.* @@ -18,7 +19,6 @@ import org.bukkit.event.player.PlayerQuitEvent import org.bukkit.inventory.Inventory import org.bukkit.inventory.InventoryHolder import org.bukkit.inventory.ItemStack -import org.bukkit.inventory.meta.EnchantmentStorageMeta import org.bukkit.permissions.Permission import org.bukkit.permissions.PermissionDefault import org.bukkit.util.BoundingBox @@ -374,26 +374,20 @@ internal class InvModule : ModuleInterface { * @param second The second ItemStack. * @return True if the enchantments match, false otherwise. */ + @Suppress("UnstableApiUsage") private fun hasMatchingEnchantments( first: ItemStack, second: ItemStack, ): Boolean { if (!config.matchEnchantments && (!config.matchEnchantmentsOnBooks || first.type != Material.ENCHANTED_BOOK)) return true - - val firstMeta = first.itemMeta - val secondMeta = second.itemMeta - - if (firstMeta == null && secondMeta == null) return true - if (firstMeta == null || secondMeta == null) return false - - if (firstMeta is EnchantmentStorageMeta && secondMeta is EnchantmentStorageMeta) { - return firstMeta.storedEnchants == secondMeta.storedEnchants - } - - if (!firstMeta.hasEnchants() && !secondMeta.hasEnchants()) return true - if (firstMeta.hasEnchants() != secondMeta.hasEnchants()) return false - - return firstMeta.enchants == secondMeta.enchants + // Gets enchantments from the ItemStack Data. + val firstEnchants = first.getData(DataComponentTypes.ENCHANTMENTS) + val secondEnchants = second.getData(DataComponentTypes.ENCHANTMENTS) + // Gets the stored enchantments from the ItemStack Data. + val firstStoredEnchants = first.getData(DataComponentTypes.STORED_ENCHANTMENTS) + val secondStoredEnchants = second.getData(DataComponentTypes.STORED_ENCHANTMENTS) + // Compares the enchantments and stored enchantments between the 2 ItemStack Data's. + return firstEnchants == secondEnchants && firstStoredEnchants == secondStoredEnchants } /** @@ -471,9 +465,7 @@ internal class InvModule : ModuleInterface { return mutableListOf().apply { for (x in minChunkX..maxChunkX) { for (z in minChunkZ..maxChunkZ) { - if (world.isChunkLoaded(x, z)) { - add(world.getChunkAt(x, z)) - } + if (world.isChunkLoaded(x, z)) add(world.getChunkAt(x, z)) } } } From a35e752b4c2861da1f0ceb0e1c65706b3eb58bf5 Mon Sep 17 00:00:00 2001 From: Illyrius Date: Mon, 10 Nov 2025 14:40:04 +0100 Subject: [PATCH 02/24] init Signed-off-by: Illyrius --- .idea/dictionaries/project.xml | 2 + .../xodium/vanillaplus/utils/BlockUtils.kt | 44 +++++ .../xodium/vanillaplus/utils/ChunkUtils.kt | 155 ++++++++++++++++++ .../org/xodium/vanillaplus/utils/InvUtils.kt | 131 +++++++++++++++ 4 files changed, 332 insertions(+) create mode 100644 src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt create mode 100644 src/main/kotlin/org/xodium/vanillaplus/utils/ChunkUtils.kt create mode 100644 src/main/kotlin/org/xodium/vanillaplus/utils/InvUtils.kt diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml index 8f06e7880..8a39c187d 100644 --- a/.idea/dictionaries/project.xml +++ b/.idea/dictionaries/project.xml @@ -5,6 +5,7 @@ armorposer birdflop cmds + coord decentholograms decentsoftware enderman @@ -29,6 +30,7 @@ redstone rrggbb searchinv + shulker signedit spellbite tellraw diff --git a/src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt b/src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt new file mode 100644 index 000000000..40fb6cec7 --- /dev/null +++ b/src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt @@ -0,0 +1,44 @@ +package org.xodium.vanillaplus.utils + +import org.bukkit.Location +import org.bukkit.block.Block +import org.bukkit.block.Chest +import org.bukkit.block.Container +import org.bukkit.block.DoubleChest +import org.bukkit.inventory.Inventory + +/** Block utilities. */ +internal object BlockUtils { + /** + * Get the centre of a block, handling double chests properly. + * @return The centre location of the block. + */ + fun Block.center(): Location { + val loc = location.clone() + val stateChest = state as? Chest ?: return loc.add(0.5, 0.5, 0.5) + val holder = stateChest.inventory.holder as? DoubleChest + if (holder != null) { + val leftLoc = (holder.leftSide as? Chest)?.block?.location + val rightLoc = (holder.rightSide as? Chest)?.block?.location + if (leftLoc != null && rightLoc != null) { + loc.x = (leftLoc.x + rightLoc.x) / 2.0 + 0.5 + loc.y = (leftLoc.y + rightLoc.y) / 2.0 + 0.5 + loc.z = (leftLoc.z + rightLoc.z) / 2.0 + 0.5 + return loc.add(0.5, 0.5, 0.5) + } + } + return loc.add(0.5, 0.5, 0.5) + } + + /** + * Check if a block is a container. + * @return True if the block is a container. + */ + fun Block.isContainer(): Boolean = state is Container + + /** + * Get the container inventory if the block is a container. + * @return The container inventory or null. + */ + fun Block.getContainerInventory(): Inventory? = (state as? Container)?.inventory +} diff --git a/src/main/kotlin/org/xodium/vanillaplus/utils/ChunkUtils.kt b/src/main/kotlin/org/xodium/vanillaplus/utils/ChunkUtils.kt new file mode 100644 index 000000000..929af56f0 --- /dev/null +++ b/src/main/kotlin/org/xodium/vanillaplus/utils/ChunkUtils.kt @@ -0,0 +1,155 @@ +@file:Suppress("ktlint:standard:no-wildcard-imports") + +package org.xodium.vanillaplus.utils + +import org.bukkit.Chunk +import org.bukkit.Location +import org.bukkit.Material +import org.bukkit.World +import org.bukkit.block.Block +import org.bukkit.block.BlockFace +import org.bukkit.block.Container +import org.bukkit.block.DoubleChest +import org.bukkit.inventory.InventoryHolder +import org.bukkit.util.BoundingBox +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +/** Chunk utilities. */ +internal object ChunkUtils { + private val cache = ConcurrentHashMap() + + private data class ChunkCoord( + val world: UUID, + val x: Int, + val z: Int, + ) + + /** + * Find all container blocks in a given radius from a location. + * @param location The location to search from. + * @param radius The radius to search within. + * @param containerTypes Set of valid container materials. + * @param containerFilter An additional filter for containers. + * @return List of container blocks found within the radius. + */ + fun findContainersInRadius( + location: Location, + radius: Int, + containerTypes: Set, + containerFilter: (Block) -> Boolean = { true }, + ): List { + val world = location.world + val centerX = location.blockX + val centerZ = location.blockZ + val radiusSquared = radius * radius + + // Calculate chunk bounds using bit shifting for performance + val minChunkX = (centerX - radius) shr 4 + val maxChunkX = (centerX + radius) shr 4 + val minChunkZ = (centerZ - radius) shr 4 + val maxChunkZ = (centerZ + radius) shr 4 + + val containers = mutableListOf() + + // Iterate through chunks first (more efficient) + for (chunkX in minChunkX..maxChunkX) { + for (chunkZ in minChunkZ..maxChunkZ) { + val chunkCoord = ChunkCoord(world.uid, chunkX, chunkZ) + + // Check if chunk is loaded using cache + if (!cache.computeIfAbsent(chunkCoord) { + world.isChunkLoaded(chunkX, chunkZ) + } + ) { + continue + } + + val chunk = world.getChunkAt(chunkX, chunkZ) + + // Process tile entities in this chunk + for (tileEntity in chunk.tileEntities) { + if (tileEntity !is Container) continue + + val block = tileEntity.block + + // Fast material check + if (!containerTypes.contains(block.type)) continue + + // Fast distance check using integer math + val dx = block.x - centerX + val dz = block.z - centerZ + if (dx * dx + dz * dz > radiusSquared) continue + + // Apply additional filters + if (containerFilter(block)) { + containers.add(block) + } + } + } + } + + // Clear cache periodically to prevent memory leaks + if (cache.size > 1000) cache.clear() + + return containers + } + + /** + * Filter out double chest duplicates and sort by distance. + * @param containers List of container blocks. + * @param centerLocation The center location for distance calculation. + * @return Filtered and sorted list of containers. + */ + fun filterAndSortContainers( + containers: List, + centerLocation: Location, + ): List { + val seenDoubleChests = mutableSetOf() + val filteredContainers = + containers.filter { block -> + val inventory = (block.state as Container).inventory + val holder = inventory.holder + if (holder is DoubleChest) seenDoubleChests.add(holder.leftSide) + true + } + + return filteredContainers.sortedBy { it.location.distanceSquared(centerLocation) } + } + + /** + * Check if a container block is accessible (not blocked, etc.) + * @param block The container block to check. + * @return True if the container is accessible. + */ + fun isContainerAccessible(block: Block): Boolean { + if (block.type == Material.CHEST) { + val blockAbove = block.getRelative(BlockFace.UP) + if (blockAbove.type.isSolid && blockAbove.type.isOccluding) return false + } + return true + } + + /** + * Get all chunks in a bounding box (legacy method for compatibility) + * @param world The world to get chunks from. + * @param box The bounding box to get chunks from. + * @return List of chunks in the bounding box. + */ + fun getChunksInBox( + world: World, + box: BoundingBox, + ): List { + val minChunkX = Math.floorDiv(box.minX.toInt(), 16) + val maxChunkX = Math.floorDiv(box.maxX.toInt(), 16) + val minChunkZ = Math.floorDiv(box.minZ.toInt(), 16) + val maxChunkZ = Math.floorDiv(box.maxZ.toInt(), 16) + return mutableListOf().apply { + for (x in minChunkX..maxChunkX) { + for (z in minChunkZ..maxChunkZ) { + if (world.isChunkLoaded(x, z)) add(world.getChunkAt(x, z)) + } + } + } + } +} diff --git a/src/main/kotlin/org/xodium/vanillaplus/utils/InvUtils.kt b/src/main/kotlin/org/xodium/vanillaplus/utils/InvUtils.kt new file mode 100644 index 000000000..de73e744e --- /dev/null +++ b/src/main/kotlin/org/xodium/vanillaplus/utils/InvUtils.kt @@ -0,0 +1,131 @@ +package org.xodium.vanillaplus.utils + +import org.bukkit.Material +import org.bukkit.Tag +import org.bukkit.block.Container +import org.bukkit.block.ShulkerBox +import org.bukkit.inventory.Inventory +import org.bukkit.inventory.ItemStack + +/** Inventory utilities. */ +internal object InvUtils { + /** + * Counts the total number of items in the given inventory. + * @param inventory The inventory to count items in. + * @return The total number of items in the inventory. + */ + fun countContents(inventory: Inventory): Int = inventory.contents.filterNotNull().sumOf { it.amount } + + /** + * Get the amount of a specific material in an inventory. + * @param inventory The inventory to check. + * @param material The material to count. + * @return The amount of the material in the inventory. + */ + fun getMaterialCount( + inventory: Inventory, + material: Material, + ): Int = + inventory.contents + .filter { it?.type == material } + .sumOf { it?.amount ?: 0 } + + /** + * Check if an inventory contains an item with the same type. + * @param inventory The inventory to check. + * @param item The item to check for. + * @return True if the inventory contains the item type, false otherwise. + */ + fun containsItemType( + inventory: Inventory, + item: ItemStack, + ): Boolean = inventory.contents.any { it?.type == item.type } + + /** + * Check if transferring an item would be valid (not putting shulker in shulker, etc.) + * @param item The item to transfer. + * @param destination The destination inventory. + * @return True if the transfer is valid, false otherwise. + */ + fun isValidTransfer( + item: ItemStack, + destination: Inventory, + ): Boolean = !(Tag.SHULKER_BOXES.isTagged(item.type) && destination.holder is ShulkerBox) + + /** + * Transfer items from source to destination inventory. + * @param source The source inventory. + * @param destination The destination inventory. + * @param startSlot The starting slot in source inventory. + * @param endSlot The ending slot in source inventory. + * @param onlyMatching If true, only transfer items that already exist in destination. + * @param enchantmentChecker Function to check if enchantments match. + * @return Pair + */ + fun transferItems( + source: Inventory, + destination: Inventory, + startSlot: Int = 9, + endSlot: Int = 35, + onlyMatching: Boolean = false, + enchantmentChecker: (ItemStack, ItemStack) -> Boolean = { _, _ -> true }, + ): Pair { + var moved = false + var totalTransferred = 0 + + for (i in startSlot..endSlot) { + val item = source.getItem(i) ?: continue + + if (!isValidTransfer(item, destination)) continue + + if (onlyMatching && !containsMatchingItem(destination, item, enchantmentChecker)) continue + + val leftovers = destination.addItem(item) + val movedAmount = item.amount - leftovers.values.sumOf { it.amount } + + if (movedAmount > 0) { + moved = true + totalTransferred += movedAmount + source.clear(i) + leftovers.values.firstOrNull()?.let { source.setItem(i, it) } + } + } + + return Pair(moved, totalTransferred) + } + + /** + * Check if inventory contains an item with matching type and enchantments. + * @param inventory The inventory to check. + * @param item The item to match. + * @param enchantmentChecker Function to check enchantment compatibility. + * @return True if a matching item is found. + */ + private fun containsMatchingItem( + inventory: Inventory, + item: ItemStack, + enchantmentChecker: (ItemStack, ItemStack) -> Boolean, + ): Boolean = + inventory.contents + .asSequence() + .filterNotNull() + .any { it.type == item.type && enchantmentChecker(item, it) } + + /** + * Search containers for specific material. + * @param containers List of containers to search. + * @param material The material to search for. + * @param enchantmentChecker Function to check enchantment compatibility. + * @return List of containers that contain the material. + */ + fun searchContainersForMaterial( + containers: List, + material: Material, + enchantmentChecker: (ItemStack, ItemStack) -> Boolean, + ): List = + containers.filter { container -> + container.inventory.contents.any { item -> + item?.type == material && enchantmentChecker(ItemStack(material), item) + } + } +} From 0659a3d093d096fee98b575eb994b0e2719e9187 Mon Sep 17 00:00:00 2001 From: Illyrius Date: Mon, 10 Nov 2025 14:54:17 +0100 Subject: [PATCH 03/24] use utils in module Signed-off-by: Illyrius --- .../xodium/vanillaplus/modules/InvModule.kt | 274 +++++------------- .../xodium/vanillaplus/utils/ChunkUtils.kt | 2 - .../org/xodium/vanillaplus/utils/InvUtils.kt | 3 +- 3 files changed, 72 insertions(+), 207 deletions(-) diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt index 5ad3ff467..193f460f5 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt @@ -10,28 +10,35 @@ import io.papermc.paper.command.brigadier.Commands import io.papermc.paper.datacomponent.DataComponentTypes import net.kyori.adventure.sound.Sound import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder -import org.bukkit.* -import org.bukkit.block.* +import org.bukkit.Color +import org.bukkit.Location +import org.bukkit.Material +import org.bukkit.Particle +import org.bukkit.block.Block +import org.bukkit.block.Container import org.bukkit.entity.Player import org.bukkit.event.EventHandler import org.bukkit.event.EventPriority import org.bukkit.event.player.PlayerQuitEvent import org.bukkit.inventory.Inventory -import org.bukkit.inventory.InventoryHolder import org.bukkit.inventory.ItemStack import org.bukkit.permissions.Permission import org.bukkit.permissions.PermissionDefault -import org.bukkit.util.BoundingBox import org.xodium.vanillaplus.VanillaPlus.Companion.instance import org.xodium.vanillaplus.data.CommandData import org.xodium.vanillaplus.data.SoundData import org.xodium.vanillaplus.interfaces.ModuleInterface import org.xodium.vanillaplus.registries.MaterialRegistry +import org.xodium.vanillaplus.utils.BlockUtils.center +import org.xodium.vanillaplus.utils.ChunkUtils.filterAndSortContainers +import org.xodium.vanillaplus.utils.ChunkUtils.findContainersInRadius +import org.xodium.vanillaplus.utils.ChunkUtils.isContainerAccessible import org.xodium.vanillaplus.utils.ExtUtils.mm import org.xodium.vanillaplus.utils.ExtUtils.tryCatch import org.xodium.vanillaplus.utils.FmtUtils.fireFmt import org.xodium.vanillaplus.utils.FmtUtils.glorpFmt import org.xodium.vanillaplus.utils.FmtUtils.roseFmt +import org.xodium.vanillaplus.utils.InvUtils.transferItems import java.util.* import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentHashMap @@ -141,27 +148,30 @@ internal class InvModule : ModuleInterface { activeVisualizations.remove(player.uniqueId) } - val chests = - findBlocksInRadius(player.location, config.searchRadius) - .filter { it.state is Container } - .filter { searchItemInContainers(material, (it.state as Container).inventory) } + val containers = + findContainersInRadius( + location = player.location, + radius = config.searchRadius, + containerTypes = MaterialRegistry.CONTAINER_TYPES, + containerFilter = ::isRelevantContainer, + ) + + val matchingContainers = + containers.filter { container -> + val inventory = (container.state as Container).inventory + inventory.contents.any { item -> + item?.type == material && hasMatchingEnchantments(ItemStack(material), item) + } + } - if (chests.isEmpty()) { - return player.sendActionBar( + if (matchingContainers.isEmpty()) { + player.sendActionBar( config.i18n.noMatchingItems.mm(Placeholder.component("material", material.name.mm())), ) + return } - val seenDoubleChests = mutableSetOf() - val filteredChests = - chests.filter { block -> - val inventory = (block.state as Container).inventory - val holder = inventory.holder - if (holder is DoubleChest && !seenDoubleChests.add(holder.leftSide)) return@filter false - true - } - - val sortedChests = filteredChests.sortedBy { it.location.distanceSquared(player.location) } + val sortedChests = filterAndSortContainers(matchingContainers, player.location) if (sortedChests.isEmpty()) return val closestChest = sortedChests.first() @@ -188,19 +198,30 @@ internal class InvModule : ModuleInterface { private fun unload(player: Player) { val startSlot = 9 val endSlot = 35 - val chests = - findBlocksInRadius(player.location, config.unloadRadius) - .filter { it.state is Container } - .sortedBy { it.location.distanceSquared(player.location) } - if (chests.isEmpty()) return player.sendActionBar(config.i18n.noNearbyChests.mm()) + val containers = + findContainersInRadius( + location = player.location, + radius = config.unloadRadius, + containerTypes = MaterialRegistry.CONTAINER_TYPES, + containerFilter = ::isRelevantContainer, + ) + + val sortedChests = filterAndSortContainers(containers, player.location) + if (sortedChests.isEmpty()) { + player.sendActionBar(config.i18n.noNearbyChests.mm()) + return + } val affectedChests = mutableListOf() - for (block in chests) { + for (block in sortedChests) { val inv = (block.state as Container).inventory - if (stuffInventoryIntoAnother(player, inv, true, startSlot, endSlot)) affectedChests.add(block) + if (performUnload(player, inv, startSlot, endSlot)) affectedChests.add(block) } - if (affectedChests.isEmpty()) return player.sendActionBar(config.i18n.noItemsUnloaded.mm()) + if (affectedChests.isEmpty()) { + player.sendActionBar(config.i18n.noItemsUnloaded.mm()) + return + } player.sendActionBar(config.i18n.inventoryUnloaded.mm()) lastUnloads[player.uniqueId] = affectedChests @@ -211,78 +232,30 @@ internal class InvModule : ModuleInterface { } /** - * Moves items from the player's inventory to another inventory. - * @param player The player whose inventory is being moved. - * @param destination The destination inventory to move items into. - * @param onlyMatchingStuff If true, only moves items that match the destination's contents. - * @param startSlot The starting slot in the player's inventory to move items from. - * @param endSlot The ending slot in the player's inventory to move items from. - * @return True if items were moved, false otherwise. + * TODO */ - private fun stuffInventoryIntoAnother( + private fun performUnload( player: Player, destination: Inventory, - onlyMatchingStuff: Boolean, startSlot: Int, endSlot: Int, ): Boolean { - val source = player.inventory - val initialCount = countInventoryContents(source) - var moved = false - - for (i in startSlot..endSlot) { - val item = source.getItem(i) ?: continue - if (Tag.SHULKER_BOXES.isTagged(item.type) && destination.holder is ShulkerBox) continue - if (onlyMatchingStuff && !doesChestContain(destination, item)) continue - - val leftovers = destination.addItem(item) - val movedAmount = item.amount - leftovers.values.sumOf { it.amount } - if (movedAmount > 0) { - moved = true - source.clear(i) - leftovers.values.firstOrNull()?.let { source.setItem(i, it) } - destination.location?.let { protocolUnload(it, item.type, movedAmount) } - } - } - return moved && initialCount != countInventoryContents(source) - } - - /** - * Counts the total number of items in the given inventory. - * @param inventory The inventory to count items in. - * @return The total number of items in the inventory. - */ - private fun countInventoryContents(inventory: Inventory): Int = inventory.contents.filterNotNull().sumOf { it.amount } + val (success, itemsTransferred) = + transferItems( + source = player.inventory, + destination = destination, + startSlot = startSlot, + endSlot = endSlot, + onlyMatching = true, + enchantmentChecker = ::hasMatchingEnchantments, + ) - /** - * Searches for a specific item in the given inventory and its containers. - * @param material The material to search for. - * @param destination The inventory to search in. - * @return True if the item was found in the inventory or its containers, false otherwise. - */ - private fun searchItemInContainers( - material: Material, - destination: Inventory, - ): Boolean { - val item = ItemStack.of(material) - val count = doesChestContainCount(destination, material) - if (count > 0 && doesChestContain(destination, item)) { - destination.location?.let { protocolUnload(it, material, count) } - return true + if (success && itemsTransferred > 0) { + destination.location?.let { location -> unloads.computeIfAbsent(location) { mutableMapOf() } } } - return false - } - /** - * Get the amount of a specific material in a chest. - * @param inventory The inventory to check. - * @param material The material to count. - * @return The amount of the material in the chest. - */ - private fun doesChestContainCount( - inventory: Inventory, - material: Material, - ): Int = inventory.contents.filter { it?.type == material }.sumOf { it?.amount ?: 0 } + return success + } /** * Schedules a repeating task for a specific player and automatically cancels it after a set duration. @@ -380,6 +353,8 @@ internal class InvModule : ModuleInterface { second: ItemStack, ): Boolean { if (!config.matchEnchantments && (!config.matchEnchantmentsOnBooks || first.type != Material.ENCHANTED_BOOK)) return true + // Early return if both items have no enchantments + if (first.enchantments.isEmpty() && second.enchantments.isEmpty()) return true // Gets enchantments from the ItemStack Data. val firstEnchants = first.getData(DataComponentTypes.ENCHANTMENTS) val secondEnchants = second.getData(DataComponentTypes.ENCHANTMENTS) @@ -391,122 +366,15 @@ internal class InvModule : ModuleInterface { } /** - * Find all blocks in a given radius from a location. - * @param location The location to search from. - * @param radius The radius to search within. - * @return A list of blocks found within the radius. - */ - private fun findBlocksInRadius( - location: Location, - radius: Int, - ): List { - val searchArea = BoundingBox.of(location, radius.toDouble(), radius.toDouble(), radius.toDouble()) - return getChunksInBox(location.world, searchArea) - .asSequence() - .flatMap { it.tileEntities.asSequence() } - .filterIsInstance() - .filter { isRelevantContainer(it, location, radius) } - .map { it.block } - .toList() - } - - /** - * Helper function to determine if a block state is a relevant container. - * @param blockState The block state to check. Must be a Container. - * @param center The centre location of the search area. - * @param radius The radius of the search area. - * @return True if the block state is a relevant container, false otherwise. + * Helper function to determine if a block is a relevant container. + * @param block The block to check. + * @return True if the block is a relevant container, false otherwise. */ - private fun isRelevantContainer( - blockState: BlockState, - center: Location, - radius: Int, - ): Boolean { - when { - blockState !is Container || !MaterialRegistry.CONTAINER_TYPES.contains(blockState.type) -> return false - blockState.location.distanceSquared(center) > radius * radius -> return false - blockState.type == Material.CHEST -> { - val blockAbove = blockState.block.getRelative(BlockFace.UP) - if (blockAbove.type.isSolid && blockAbove.type.isOccluding) return false - } - } + private fun isRelevantContainer(block: Block): Boolean { + if (block.type == Material.CHEST) return isContainerAccessible(block) return true } - /** - * Check if a chest contains an item with matching enchantments. - * @param inventory The inventory to check. - * @param item The item to check for. - * @return True if the chest contains the item, false otherwise. - */ - private fun doesChestContain( - inventory: Inventory, - item: ItemStack, - ): Boolean = - inventory.contents - .asSequence() - .filterNotNull() - .any { it.type == item.type && hasMatchingEnchantments(item, it) } - - /** - * Get all chunks in a bounding box. - * @param world The world to get chunks from. - * @param box The bounding box to get chunks from. - * @return A list of chunks in the bounding box. - */ - private fun getChunksInBox( - world: World, - box: BoundingBox, - ): List { - val minChunkX = Math.floorDiv(box.minX.toInt(), 16) - val maxChunkX = Math.floorDiv(box.maxX.toInt(), 16) - val minChunkZ = Math.floorDiv(box.minZ.toInt(), 16) - val maxChunkZ = Math.floorDiv(box.maxZ.toInt(), 16) - return mutableListOf().apply { - for (x in minChunkX..maxChunkX) { - for (z in minChunkZ..maxChunkZ) { - if (world.isChunkLoaded(x, z)) add(world.getChunkAt(x, z)) - } - } - } - } - - /** - * Unloads the specified amount of material from the given location. - * @param location The location to unload from. - * @param material The material to unload. - * @param amount The amount of material to unload. - */ - private fun protocolUnload( - location: Location, - material: Material, - amount: Int, - ) { - if (amount == 0) return - unloads.computeIfAbsent(location) { mutableMapOf() }.merge(material, amount, Int::plus) - } - - /** - * Get the centre of a block. - * @return The centre location of the block. - */ - private fun Block.center(): Location { - val loc = location.clone() - val stateChest = state as? Chest ?: return loc.add(0.5, 0.5, 0.5) - val holder = stateChest.inventory.holder as? DoubleChest - if (holder != null) { - val leftLoc = (holder.leftSide as? Chest)?.block?.location - val rightLoc = (holder.rightSide as? Chest)?.block?.location - if (leftLoc != null && rightLoc != null) { - loc.x = (leftLoc.x + rightLoc.x) / 2.0 + 0.5 - loc.y = (leftLoc.y + rightLoc.y) / 2.0 + 0.5 - loc.z = (leftLoc.z + rightLoc.z) / 2.0 + 0.5 - return loc.add(0.5, 0.5, 0.5) - } - } - return loc.add(0.5, 0.5, 0.5) - } - data class Config( override var enabled: Boolean = true, var searchRadius: Int = 25, diff --git a/src/main/kotlin/org/xodium/vanillaplus/utils/ChunkUtils.kt b/src/main/kotlin/org/xodium/vanillaplus/utils/ChunkUtils.kt index 929af56f0..5e7c3cd81 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/utils/ChunkUtils.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/utils/ChunkUtils.kt @@ -43,13 +43,11 @@ internal object ChunkUtils { val centerX = location.blockX val centerZ = location.blockZ val radiusSquared = radius * radius - // Calculate chunk bounds using bit shifting for performance val minChunkX = (centerX - radius) shr 4 val maxChunkX = (centerX + radius) shr 4 val minChunkZ = (centerZ - radius) shr 4 val maxChunkZ = (centerZ + radius) shr 4 - val containers = mutableListOf() // Iterate through chunks first (more efficient) diff --git a/src/main/kotlin/org/xodium/vanillaplus/utils/InvUtils.kt b/src/main/kotlin/org/xodium/vanillaplus/utils/InvUtils.kt index de73e744e..c2d51a1ce 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/utils/InvUtils.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/utils/InvUtils.kt @@ -58,7 +58,7 @@ internal object InvUtils { * @param destination The destination inventory. * @param startSlot The starting slot in source inventory. * @param endSlot The ending slot in source inventory. - * @param onlyMatching If true, only transfer items that already exist in destination. + * @param onlyMatching If true, only transfer items that already exist in the destination. * @param enchantmentChecker Function to check if enchantments match. * @return Pair */ @@ -77,7 +77,6 @@ internal object InvUtils { val item = source.getItem(i) ?: continue if (!isValidTransfer(item, destination)) continue - if (onlyMatching && !containsMatchingItem(destination, item, enchantmentChecker)) continue val leftovers = destination.addItem(item) From 811ce339606651eb58831e4cd262bb4a62f2ea9d Mon Sep 17 00:00:00 2001 From: Illyrius Date: Tue, 11 Nov 2025 10:47:59 +0100 Subject: [PATCH 04/24] refactor: update particle effects and clean up unused code in InvModule Signed-off-by: Illyrius --- ...powered_velocity_native_3_4_0_SNAPSHOT.xml | 2 +- ..._paper_paper_api_1_21_10_R0_1_SNAPSHOT.xml | 4 +- .idea/modules/VanillaPlus.main.iml | 5 +- .idea/modules/VanillaPlus.test.iml | 5 +- .../xodium/vanillaplus/modules/InvModule.kt | 107 +++++++++--------- 5 files changed, 59 insertions(+), 64 deletions(-) diff --git a/.idea/libraries/Gradle__com_velocitypowered_velocity_native_3_4_0_SNAPSHOT.xml b/.idea/libraries/Gradle__com_velocitypowered_velocity_native_3_4_0_SNAPSHOT.xml index 7f613a1a8..89cfc6ea4 100644 --- a/.idea/libraries/Gradle__com_velocitypowered_velocity_native_3_4_0_SNAPSHOT.xml +++ b/.idea/libraries/Gradle__com_velocitypowered_velocity_native_3_4_0_SNAPSHOT.xml @@ -2,7 +2,7 @@ - + diff --git a/.idea/libraries/Gradle__io_papermc_paper_paper_api_1_21_10_R0_1_SNAPSHOT.xml b/.idea/libraries/Gradle__io_papermc_paper_paper_api_1_21_10_R0_1_SNAPSHOT.xml index 07eaf6178..9cef7876f 100644 --- a/.idea/libraries/Gradle__io_papermc_paper_paper_api_1_21_10_R0_1_SNAPSHOT.xml +++ b/.idea/libraries/Gradle__io_papermc_paper_paper_api_1_21_10_R0_1_SNAPSHOT.xml @@ -2,11 +2,11 @@ - + - + \ No newline at end of file diff --git a/.idea/modules/VanillaPlus.main.iml b/.idea/modules/VanillaPlus.main.iml index 5674d84be..7a22db58e 100644 --- a/.idea/modules/VanillaPlus.main.iml +++ b/.idea/modules/VanillaPlus.main.iml @@ -4,7 +4,6 @@ - PAPER MCP ADVENTURE @@ -12,7 +11,7 @@ - + @@ -56,7 +55,7 @@ - + diff --git a/.idea/modules/VanillaPlus.test.iml b/.idea/modules/VanillaPlus.test.iml index 887be16db..28546c8b1 100644 --- a/.idea/modules/VanillaPlus.test.iml +++ b/.idea/modules/VanillaPlus.test.iml @@ -4,7 +4,6 @@ - PAPER MCP ADVENTURE @@ -12,7 +11,7 @@ - + VanillaPlus:main @@ -54,7 +53,7 @@ - + diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt index 193f460f5..9960b46fd 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt @@ -2,7 +2,6 @@ package org.xodium.vanillaplus.modules -import com.destroystokyo.paper.ParticleBuilder import com.mojang.brigadier.arguments.StringArgumentType import com.mojang.brigadier.context.CommandContext import io.papermc.paper.command.brigadier.CommandSourceStack @@ -111,6 +110,7 @@ internal class InvModule : ModuleInterface { if (!enabled()) return val uuid = event.player.uniqueId + lastUnloads.remove(uuid) activeVisualizations.remove(uuid) } @@ -125,12 +125,14 @@ internal class InvModule : ModuleInterface { val materialName = runCatching { StringArgumentType.getString(ctx, "material") }.getOrNull() val material = materialName?.let { Material.getMaterial(it.uppercase()) } ?: player.inventory.itemInMainHand.type + if (material == Material.AIR) { player.sendActionBar(config.i18n.noMaterialSpecified.mm()) return 0 } search(player, material) + return 1 } @@ -155,7 +157,6 @@ internal class InvModule : ModuleInterface { containerTypes = MaterialRegistry.CONTAINER_TYPES, containerFilter = ::isRelevantContainer, ) - val matchingContainers = containers.filter { container -> val inventory = (container.state as Container).inventory @@ -172,20 +173,45 @@ internal class InvModule : ModuleInterface { } val sortedChests = filterAndSortContainers(matchingContainers, player.location) + if (sortedChests.isEmpty()) return val closestChest = sortedChests.first() + schedulePlayerTask(player, { - searchEffect(player.location, closestChest.center(), Color.MAROON, 40, player) - chestEffect(closestChest.center(), 10, Particle.DustOptions(Color.MAROON, 5.0f), player) + Particle.TRAIL + .builder() + .location(player.location) + .data(Particle.Trail(closestChest.center(), Color.MAROON, 40)) + .receivers(player) + .spawn() + Particle.DUST + .builder() + .location(closestChest.center()) + .count(10) + .data(Particle.DustOptions(Color.MAROON, 5.0f)) + .receivers(player) + .spawn() }) val otherChests = sortedChests.drop(1) + if (otherChests.isNotEmpty()) { schedulePlayerTask(player, { otherChests.forEach { - searchEffect(player.location, it.center(), Color.RED, 40, player) - chestEffect(it.center(), 10, Particle.DustOptions(Color.RED, 5.0f), player) + Particle.TRAIL + .builder() + .location(player.location) + .data(Particle.Trail(it.center(), Color.RED, 40)) + .receivers(player) + .spawn() + Particle.DUST + .builder() + .location(it.center()) + .count(10) + .data(Particle.DustOptions(Color.RED, 5.0f)) + .receivers(player) + .spawn() } }) } @@ -205,14 +231,15 @@ internal class InvModule : ModuleInterface { containerTypes = MaterialRegistry.CONTAINER_TYPES, containerFilter = ::isRelevantContainer, ) - val sortedChests = filterAndSortContainers(containers, player.location) + if (sortedChests.isEmpty()) { player.sendActionBar(config.i18n.noNearbyChests.mm()) return } val affectedChests = mutableListOf() + for (block in sortedChests) { val inv = (block.state as Container).inventory if (performUnload(player, inv, startSlot, endSlot)) affectedChests.add(block) @@ -226,13 +253,29 @@ internal class InvModule : ModuleInterface { player.sendActionBar(config.i18n.inventoryUnloaded.mm()) lastUnloads[player.uniqueId] = affectedChests - for (chest in affectedChests) chestEffect(chest.center(), 10, Particle.DustOptions(Color.LIME, 5.0f), player) + for (chest in affectedChests) { + Particle.DUST + .builder() + .location(chest.center()) + .count(10) + .data(Particle.DustOptions(Color.LIME, 5.0f)) + .receivers(player) + .spawn() + } player.playSound(config.soundOnUnload.toSound(), Sound.Emitter.self()) } /** - * TODO + * Transfers items from the specified player's inventory to the destination inventory within the given slot range. + * + * Only items matching certain criteria (e.g., matching enchantments) are transferred. + * + * @param player The player whose inventory items are to be transferred. + * @param destination The inventory to which items will be transferred. + * @param startSlot The starting slot index in the player's inventory to consider for transfer. + * @param endSlot The ending slot index in the player's inventory to consider for transfer. + * @return `true` if any items were successfully transferred, `false` otherwise. */ private fun performUnload( player: Player, @@ -295,52 +338,6 @@ internal class InvModule : ModuleInterface { return taskId } - /** - * Spawns a particle trail effect visible only to the given player. - * @param startLocation The starting location of the trail. - * @param endLocation The ending location of the trail. - * @param color The colour of the trail. - * @param travelTicks The number of ticks it takes for the trail to travel from start to end. - * @param player The player who will see the particle trail. - * @return A [ParticleBuilder] instance for further configuration if needed. - */ - private fun searchEffect( - startLocation: Location, - endLocation: Location, - color: Color, - travelTicks: Int, - player: Player, - ): ParticleBuilder = - Particle.TRAIL - .builder() - .location(startLocation) - .data(Particle.Trail(endLocation, color, travelTicks)) - .receivers(player) - .spawn() - - /** - * Spawns a critical hit particle effect at the given location, - * visible only to the specified player. - * @param location The central [Location] where the particles will appear. - * @param count The number of particles to spawn. - * @param dustOptions The [Particle.DustOptions] defining the colour and size of the dust particles. - * @param player The [Player] who will see the particle effect. - * @return A [ParticleBuilder] instance for further configuration if needed. - */ - private fun chestEffect( - location: Location, - count: Int, - dustOptions: Particle.DustOptions, - player: Player, - ): ParticleBuilder = - Particle.DUST - .builder() - .location(location) - .count(count) - .data(dustOptions) - .receivers(player) - .spawn() - /** * Checks if two ItemStacks have matching enchantments. * @param first The first ItemStack. From bfe3b19db648a9897bc98428d6da63e32618d6fa Mon Sep 17 00:00:00 2001 From: Illyrius Date: Tue, 11 Nov 2025 11:43:13 +0100 Subject: [PATCH 05/24] refactor: simplify block utility functions and improve property access Signed-off-by: Illyrius --- .../vanillaplus/engines/ExpressionEngine.kt | 45 ------------------- .../vanillaplus/interfaces/DataInterface.kt | 4 +- .../vanillaplus/managers/ConfigManager.kt | 2 +- .../xodium/vanillaplus/modules/ChatModule.kt | 2 +- .../xodium/vanillaplus/modules/InvModule.kt | 16 +++---- .../org/xodium/vanillaplus/pdcs/HorsePDC.kt | 29 ------------ .../xodium/vanillaplus/utils/BlockUtils.kt | 43 +++++++----------- .../org/xodium/vanillaplus/utils/ExtUtils.kt | 38 ++++++++-------- 8 files changed, 44 insertions(+), 135 deletions(-) delete mode 100644 src/main/kotlin/org/xodium/vanillaplus/engines/ExpressionEngine.kt delete mode 100644 src/main/kotlin/org/xodium/vanillaplus/pdcs/HorsePDC.kt diff --git a/src/main/kotlin/org/xodium/vanillaplus/engines/ExpressionEngine.kt b/src/main/kotlin/org/xodium/vanillaplus/engines/ExpressionEngine.kt deleted file mode 100644 index df875c96b..000000000 --- a/src/main/kotlin/org/xodium/vanillaplus/engines/ExpressionEngine.kt +++ /dev/null @@ -1,45 +0,0 @@ -package org.xodium.vanillaplus.engines - -import org.mariuszgromada.math.mxparser.Argument -import org.mariuszgromada.math.mxparser.Expression - -/** - * A mathematical expression engine that safely evaluates string expressions using mXparser. - * @see org.mariuszgromada.math.mxparser.Expression - * @see org.mariuszgromada.math.mxparser.Argument - */ -internal object ExpressionEngine { - /** - * Evaluates a mathematical expression with the provided variable context. - * @param expression the mathematical expression to evaluate (e.g. "speed * 10 + jump * 5"). - * @param context a map of variable names to their values for expression substitution. - * @return the computed numerical result of the expression. - * @throws IllegalArgumentException if: - * — Expression contains forbidden characters (`;`, `{`, `}`, `[`, `]`, `"`) :cite[1] - * — Expression syntax is invalid - * — Result is not a valid number (NaN or infinite) - */ - fun evaluate( - expression: String, - context: Map, - ): Double { - if (expression.contains(Regex("""[;{}\[\]"]"""))) { - throw IllegalArgumentException("Expression contains forbidden characters") - } - - val expr = Expression(expression) - - context.forEach { (key, value) -> expr.addArguments(Argument("$key = $value")) } - - val result = expr.calculate() - - if (expr.checkSyntax()) { - if (result.isNaN() || result.isInfinite()) { - throw IllegalArgumentException("Expression result is not a valid number") - } - return result - } else { - throw IllegalArgumentException("Syntax error in expression: ${expr.errorMessage}") - } - } -} diff --git a/src/main/kotlin/org/xodium/vanillaplus/interfaces/DataInterface.kt b/src/main/kotlin/org/xodium/vanillaplus/interfaces/DataInterface.kt index 3c5fdc631..1928ceede 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/interfaces/DataInterface.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/interfaces/DataInterface.kt @@ -12,7 +12,7 @@ import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.KotlinModule import org.xodium.vanillaplus.VanillaPlus.Companion.instance -import org.xodium.vanillaplus.utils.ExtUtils.toSnakeCase +import org.xodium.vanillaplus.utils.ExtUtils.snakeCase import java.io.IOException import java.nio.file.Path import kotlin.io.path.createDirectories @@ -24,7 +24,7 @@ interface DataInterface { val dataClass: KClass val cache: MutableMap val fileName: String - get() = "${dataClass.simpleName?.toSnakeCase()}.json" + get() = "${dataClass.simpleName?.snakeCase}.json" val filePath: Path get() = instance.dataFolder.toPath().resolve(fileName) val jsonMapper: ObjectMapper diff --git a/src/main/kotlin/org/xodium/vanillaplus/managers/ConfigManager.kt b/src/main/kotlin/org/xodium/vanillaplus/managers/ConfigManager.kt index 6025a2d84..631efe340 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/managers/ConfigManager.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/managers/ConfigManager.kt @@ -19,7 +19,7 @@ internal object ConfigManager : DataInterface { */ fun update(modules: List>) { modules.forEach { module -> - val key = module.key() + val key = module.key val fileConfig = readFileConfig(key, module) val mergedConfig = fileConfig?.let { jsonMapper.updateValue(module.config, it) } ?: module.config set(key, mergedConfig) diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/ChatModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/ChatModule.kt index 67e6ba915..66af3d663 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/ChatModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/ChatModule.kt @@ -122,7 +122,7 @@ internal class ChatModule : ModuleInterface { .clickEvent(ClickEvent.suggestCommand("/w ${player.name} ")) .hoverEvent(HoverEvent.showText(config.i18n.clickToWhisper.mm())), ), - Placeholder.component("message", message.pt().mm()), + Placeholder.component("message", message.pt.mm()), ) if (audience == player) base = base.appendSpace().append(createDeleteCross(event)) base diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt index 9960b46fd..f20a824f5 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt @@ -159,8 +159,7 @@ internal class InvModule : ModuleInterface { ) val matchingContainers = containers.filter { container -> - val inventory = (container.state as Container).inventory - inventory.contents.any { item -> + (container as Container).inventory.contents.any { item -> item?.type == material && hasMatchingEnchantments(ItemStack(material), item) } } @@ -182,12 +181,12 @@ internal class InvModule : ModuleInterface { Particle.TRAIL .builder() .location(player.location) - .data(Particle.Trail(closestChest.center(), Color.MAROON, 40)) + .data(Particle.Trail(closestChest.center, Color.MAROON, 40)) .receivers(player) .spawn() Particle.DUST .builder() - .location(closestChest.center()) + .location(closestChest.center) .count(10) .data(Particle.DustOptions(Color.MAROON, 5.0f)) .receivers(player) @@ -202,12 +201,12 @@ internal class InvModule : ModuleInterface { Particle.TRAIL .builder() .location(player.location) - .data(Particle.Trail(it.center(), Color.RED, 40)) + .data(Particle.Trail(it.center, Color.RED, 40)) .receivers(player) .spawn() Particle.DUST .builder() - .location(it.center()) + .location(it.center) .count(10) .data(Particle.DustOptions(Color.RED, 5.0f)) .receivers(player) @@ -241,8 +240,7 @@ internal class InvModule : ModuleInterface { val affectedChests = mutableListOf() for (block in sortedChests) { - val inv = (block.state as Container).inventory - if (performUnload(player, inv, startSlot, endSlot)) affectedChests.add(block) + if (performUnload(player, (block as Container).inventory, startSlot, endSlot)) affectedChests.add(block) } if (affectedChests.isEmpty()) { @@ -256,7 +254,7 @@ internal class InvModule : ModuleInterface { for (chest in affectedChests) { Particle.DUST .builder() - .location(chest.center()) + .location(chest.center) .count(10) .data(Particle.DustOptions(Color.LIME, 5.0f)) .receivers(player) diff --git a/src/main/kotlin/org/xodium/vanillaplus/pdcs/HorsePDC.kt b/src/main/kotlin/org/xodium/vanillaplus/pdcs/HorsePDC.kt deleted file mode 100644 index d24284d22..000000000 --- a/src/main/kotlin/org/xodium/vanillaplus/pdcs/HorsePDC.kt +++ /dev/null @@ -1,29 +0,0 @@ -package org.xodium.vanillaplus.pdcs - -import org.bukkit.NamespacedKey -import org.bukkit.entity.Horse -import org.bukkit.persistence.PersistentDataType -import org.xodium.vanillaplus.VanillaPlus.Companion.instance -import org.xodium.vanillaplus.pdcs.HorsePDC.SOLD_KEY - -/** - * Provides access to horse-specific persistent data including sold status. - * @property SOLD_KEY The namespaced key used for storing sold status data. - */ -internal object HorsePDC { - private val SOLD_KEY = NamespacedKey(instance, "horse_sold") - - /** - * Retrieves the sold status of this horse from its persistent data container. - * @receiver The horse whose sold status to retrieve. - * @return `true` if the horse is marked as sold, `false` if not, or null if the data is not set. - */ - fun Horse.sold(): Boolean = persistentDataContainer.get(SOLD_KEY, PersistentDataType.BOOLEAN) ?: false - - /** - * Sets the sold status of this horse in its persistent data container. - * @receiver The horse whose sold status to modify. - * @param boolean The sold state to set (`true` for sold, `false` for not sold). - */ - fun Horse.sold(boolean: Boolean) = persistentDataContainer.set(SOLD_KEY, PersistentDataType.BOOLEAN, boolean) -} diff --git a/src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt b/src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt index 40fb6cec7..d4359578d 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt @@ -3,9 +3,7 @@ package org.xodium.vanillaplus.utils import org.bukkit.Location import org.bukkit.block.Block import org.bukkit.block.Chest -import org.bukkit.block.Container import org.bukkit.block.DoubleChest -import org.bukkit.inventory.Inventory /** Block utilities. */ internal object BlockUtils { @@ -13,32 +11,21 @@ internal object BlockUtils { * Get the centre of a block, handling double chests properly. * @return The centre location of the block. */ - fun Block.center(): Location { - val loc = location.clone() - val stateChest = state as? Chest ?: return loc.add(0.5, 0.5, 0.5) - val holder = stateChest.inventory.holder as? DoubleChest - if (holder != null) { - val leftLoc = (holder.leftSide as? Chest)?.block?.location - val rightLoc = (holder.rightSide as? Chest)?.block?.location - if (leftLoc != null && rightLoc != null) { - loc.x = (leftLoc.x + rightLoc.x) / 2.0 + 0.5 - loc.y = (leftLoc.y + rightLoc.y) / 2.0 + 0.5 - loc.z = (leftLoc.z + rightLoc.z) / 2.0 + 0.5 - return loc.add(0.5, 0.5, 0.5) - } - } - return loc.add(0.5, 0.5, 0.5) - } + val Block.center: Location + get() { + val baseAddition = Location(location.world, location.x + 0.5, location.y + 0.5, location.z + 0.5) + val chestState = state as? Chest ?: return baseAddition + val holder = chestState.inventory.holder as? DoubleChest ?: return baseAddition + val leftBlock = (holder.leftSide as? Chest)?.block + val rightBlock = (holder.rightSide as? Chest)?.block - /** - * Check if a block is a container. - * @return True if the block is a container. - */ - fun Block.isContainer(): Boolean = state is Container + if (leftBlock == null || rightBlock == null || leftBlock.world !== rightBlock.world) return baseAddition - /** - * Get the container inventory if the block is a container. - * @return The container inventory or null. - */ - fun Block.getContainerInventory(): Inventory? = (state as? Container)?.inventory + val world = leftBlock.world + val cx = (leftBlock.x + rightBlock.x) / 2.0 + 0.5 + val cy = (leftBlock.y + rightBlock.y) / 2.0 + 0.5 + val cz = (leftBlock.z + rightBlock.z) / 2.0 + 0.5 + + return Location(world, cx, cy, cz) + } } diff --git a/src/main/kotlin/org/xodium/vanillaplus/utils/ExtUtils.kt b/src/main/kotlin/org/xodium/vanillaplus/utils/ExtUtils.kt index 53eeedfe5..40e058a5e 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/utils/ExtUtils.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/utils/ExtUtils.kt @@ -36,9 +36,25 @@ internal object ExtUtils { private const val RED_SHIFT = 16 private const val GREEN_SHIFT = 8 + /** The standardized prefix for [VanillaPlus] messages. */ val VanillaPlus.prefix: String get() = "${"[".mangoFmt(true)}${this::class.simpleName.toString().fireFmt()}${"]".mangoFmt()}" + /** Serializes a [Component] into plaintext. */ + val Component.pt: String get() = PlainTextComponentSerializer.plainText().serialize(this) + + /** + * Converts a CamelCase string to snake case. + * @return the snake case version of the string. + */ + val String.snakeCase: String get() = replace(Regex("([a-z])([A-Z])"), "$1_$2").lowercase() + + /** + * Generates a configuration key for a module. + * @return The generated configuration key. + */ + val ModuleInterface<*>.key: String get() = this::class.simpleName.toString() + /** * Deserializes a [MiniMessage] [String] into a [Component]. * @param resolvers Optional tag resolvers for custom tags. @@ -59,12 +75,6 @@ internal object ExtUtils { @JvmName("mmStringIterable") fun Iterable.mm(vararg resolvers: TagResolver): List = map { it.mm(*resolvers) } - /** Serializes a [Component] into a String. */ - fun Component.mm(): String = MM.serialize(this) - - /** Serializes a [Component] into plaintext. */ - fun Component.pt(): String = PlainTextComponentSerializer.plainText().serialize(this) - /** * Performs a command from a [String]. * @param hover Optional hover text for the command. @@ -83,7 +93,7 @@ internal object ExtUtils { * @return Command.SINGLE_SUCCESS after execution. */ fun CommandContext.tryCatch(action: (CommandSourceStack) -> Unit): Int { - runCatching { action(this.source) } + runCatching { action(source) } .onFailure { e -> instance.logger.severe( """ @@ -91,7 +101,7 @@ internal object ExtUtils { ${e.stackTraceToString()} """.trimIndent(), ) - (this.source.sender as? Player)?.sendMessage( + (source.sender as? Player)?.sendMessage( "${instance.prefix} An error has occurred. Check server logs for details.".mm(), ) } @@ -143,16 +153,4 @@ internal object ExtUtils { } return builder.toString() } - - /** - * Converts a CamelCase string to snake case. - * @return the snake case version of the string. - */ - fun String.toSnakeCase(): String = this.replace(Regex("([a-z])([A-Z])"), "$1_$2").lowercase() - - /** - * Generates a configuration key for a module. - * @return The generated configuration key. - */ - fun ModuleInterface<*>.key(): String = this::class.simpleName.toString() } From c52b88ebcc0e1443b265ca58f5a7c2a8183c7131 Mon Sep 17 00:00:00 2001 From: Illyrius Date: Tue, 11 Nov 2025 11:58:38 +0100 Subject: [PATCH 06/24] refactor: convert nickname and scoreboard visibility functions to properties for improved access Signed-off-by: Illyrius --- .../vanillaplus/modules/PlayerModule.kt | 6 +-- .../vanillaplus/modules/ScoreBoardModule.kt | 8 +-- .../org/xodium/vanillaplus/pdcs/PlayerPDC.kt | 52 ++++++++----------- 3 files changed, 30 insertions(+), 36 deletions(-) diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/PlayerModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/PlayerModule.kt index 0b38b8468..8fbc907a5 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/PlayerModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/PlayerModule.kt @@ -77,7 +77,7 @@ internal class PlayerModule( fun on(event: PlayerJoinEvent) { if (!enabled()) return val player = event.player - player.displayName(player.nickname()?.mm()) + player.displayName(player.nickname?.mm()) if (config.i18n.playerJoinMsg.isEmpty()) return event.joinMessage(null) @@ -180,8 +180,8 @@ internal class PlayerModule( player: Player, name: String, ) { - player.nickname(name) - player.displayName(player.nickname()?.mm()) + player.nickname = name + player.displayName(player.nickname?.mm()) tabListModule.updatePlayerDisplayName(player) player.sendActionBar(config.i18n.nicknameUpdated.mm(Placeholder.component("nickname", player.displayName()))) } diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/ScoreBoardModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/ScoreBoardModule.kt index 35906a214..cf4f2aa44 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/ScoreBoardModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/ScoreBoardModule.kt @@ -46,7 +46,7 @@ internal class ScoreBoardModule : ModuleInterface { fun on(event: PlayerJoinEvent) { if (!enabled()) return val player = event.player - if (player.scoreboardVisibility() == true) { + if (player.scoreboardVisibility == true) { player.scoreboard = instance.server.scoreboardManager.newScoreboard } else { player.scoreboard = instance.server.scoreboardManager.mainScoreboard @@ -58,12 +58,12 @@ internal class ScoreBoardModule : ModuleInterface { * @param player The player whose scoreboard sidebar should be toggled. */ private fun toggle(player: Player) { - if (player.scoreboardVisibility() == true) { + if (player.scoreboardVisibility == true) { player.scoreboard = instance.server.scoreboardManager.mainScoreboard - player.scoreboardVisibility(false) + player.scoreboardVisibility = false } else { player.scoreboard = instance.server.scoreboardManager.newScoreboard - player.scoreboardVisibility(true) + player.scoreboardVisibility = true } } diff --git a/src/main/kotlin/org/xodium/vanillaplus/pdcs/PlayerPDC.kt b/src/main/kotlin/org/xodium/vanillaplus/pdcs/PlayerPDC.kt index e8a802d1b..c4146c5bb 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/pdcs/PlayerPDC.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/pdcs/PlayerPDC.kt @@ -17,38 +17,32 @@ internal object PlayerPDC { private val SCOREBOARD_VISIBILITY_KEY = NamespacedKey(instance, "scoreboard_visibility") /** - * Retrieves the player's nickname from their persistent data container. - * @receiver The player whose nickname to retrieve. - * @return The player's nickname, or null if no nickname is set. + * Gets or sets the player's nickname in their persistent data container. + * @receiver The player whose nickname to access. + * @return The player's nickname, or null if not set. */ - fun Player.nickname(): String? = persistentDataContainer.get(NICKNAME_KEY, PersistentDataType.STRING) - - /** - * Sets or removes the player's nickname in their persistent data container. - * @receiver The player whose nickname to modify. - * @param name The nickname to set, or null/empty to remove the current nickname. - */ - fun Player.nickname(name: String?) { - if (name.isNullOrEmpty()) { - persistentDataContainer.remove(NICKNAME_KEY) - } else { - persistentDataContainer.set(NICKNAME_KEY, PersistentDataType.STRING, name) + var Player.nickname: String? + get() = persistentDataContainer.get(NICKNAME_KEY, PersistentDataType.STRING) + set(value) { + if (value.isNullOrEmpty()) { + persistentDataContainer.remove(NICKNAME_KEY) + } else { + persistentDataContainer.set(NICKNAME_KEY, PersistentDataType.STRING, value) + } } - } /** - * Retrieves the player's scoreboard visibility setting from their persistent data container. - * @receiver The player whose scoreboard visibility to check. - * @return The scoreboard visibility state, or null if not set. + * Gets or sets the player's scoreboard visibility preference in their persistent data container. + * @receiver The player whose scoreboard visibility to access. + * @return True if the scoreboard is visible, false if hidden, or null if not set. */ - fun Player.scoreboardVisibility(): Boolean? = persistentDataContainer.get(SCOREBOARD_VISIBILITY_KEY, PersistentDataType.BOOLEAN) - - /** - * Sets the player's scoreboard visibility in their persistent data container. - * @receiver The player whose scoreboard visibility to modify. - * @param visible The visibility state to set (true for visible, false for hidden). - */ - fun Player.scoreboardVisibility(visible: Boolean) { - persistentDataContainer.set(SCOREBOARD_VISIBILITY_KEY, PersistentDataType.BOOLEAN, visible) - } + var Player.scoreboardVisibility: Boolean? + get() = persistentDataContainer.get(SCOREBOARD_VISIBILITY_KEY, PersistentDataType.BOOLEAN) + set(value) { + if (value == null) { + persistentDataContainer.remove(SCOREBOARD_VISIBILITY_KEY) + } else { + persistentDataContainer.set(SCOREBOARD_VISIBILITY_KEY, PersistentDataType.BOOLEAN, value) + } + } } From 88907e010778c4cad016943c6482a61346607c24 Mon Sep 17 00:00:00 2001 From: Illyrius Date: Tue, 11 Nov 2025 12:24:49 +0100 Subject: [PATCH 07/24] refactor: add extension property to check container block accessibility Signed-off-by: Illyrius --- .../org/xodium/vanillaplus/modules/InvModule.kt | 16 +++------------- .../org/xodium/vanillaplus/utils/BlockUtils.kt | 9 +++++++++ .../org/xodium/vanillaplus/utils/ChunkUtils.kt | 14 -------------- 3 files changed, 12 insertions(+), 27 deletions(-) diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt index f20a824f5..8e5e50caf 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt @@ -29,9 +29,9 @@ import org.xodium.vanillaplus.data.SoundData import org.xodium.vanillaplus.interfaces.ModuleInterface import org.xodium.vanillaplus.registries.MaterialRegistry import org.xodium.vanillaplus.utils.BlockUtils.center +import org.xodium.vanillaplus.utils.BlockUtils.isContainerAccessible import org.xodium.vanillaplus.utils.ChunkUtils.filterAndSortContainers import org.xodium.vanillaplus.utils.ChunkUtils.findContainersInRadius -import org.xodium.vanillaplus.utils.ChunkUtils.isContainerAccessible import org.xodium.vanillaplus.utils.ExtUtils.mm import org.xodium.vanillaplus.utils.ExtUtils.tryCatch import org.xodium.vanillaplus.utils.FmtUtils.fireFmt @@ -155,7 +155,7 @@ internal class InvModule : ModuleInterface { location = player.location, radius = config.searchRadius, containerTypes = MaterialRegistry.CONTAINER_TYPES, - containerFilter = ::isRelevantContainer, + containerFilter = { it.isContainerAccessible }, ) val matchingContainers = containers.filter { container -> @@ -228,7 +228,7 @@ internal class InvModule : ModuleInterface { location = player.location, radius = config.unloadRadius, containerTypes = MaterialRegistry.CONTAINER_TYPES, - containerFilter = ::isRelevantContainer, + containerFilter = { it.isContainerAccessible }, ) val sortedChests = filterAndSortContainers(containers, player.location) @@ -360,16 +360,6 @@ internal class InvModule : ModuleInterface { return firstEnchants == secondEnchants && firstStoredEnchants == secondStoredEnchants } - /** - * Helper function to determine if a block is a relevant container. - * @param block The block to check. - * @return True if the block is a relevant container, false otherwise. - */ - private fun isRelevantContainer(block: Block): Boolean { - if (block.type == Material.CHEST) return isContainerAccessible(block) - return true - } - data class Config( override var enabled: Boolean = true, var searchRadius: Int = 25, diff --git a/src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt b/src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt index d4359578d..4c85b0170 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt @@ -1,7 +1,9 @@ package org.xodium.vanillaplus.utils import org.bukkit.Location +import org.bukkit.Material import org.bukkit.block.Block +import org.bukkit.block.BlockFace import org.bukkit.block.Chest import org.bukkit.block.DoubleChest @@ -28,4 +30,11 @@ internal object BlockUtils { return Location(world, cx, cy, cz) } + + /** + * Check if a container block is accessible (not blocked from above). + * @return True if the container is accessible. + */ + val Block.isContainerAccessible: Boolean + get() = type != Material.CHEST || !getRelative(BlockFace.UP).type.let { it.isSolid && it.isOccluding } } diff --git a/src/main/kotlin/org/xodium/vanillaplus/utils/ChunkUtils.kt b/src/main/kotlin/org/xodium/vanillaplus/utils/ChunkUtils.kt index 5e7c3cd81..a0981857d 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/utils/ChunkUtils.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/utils/ChunkUtils.kt @@ -7,7 +7,6 @@ import org.bukkit.Location import org.bukkit.Material import org.bukkit.World import org.bukkit.block.Block -import org.bukkit.block.BlockFace import org.bukkit.block.Container import org.bukkit.block.DoubleChest import org.bukkit.inventory.InventoryHolder @@ -115,19 +114,6 @@ internal object ChunkUtils { return filteredContainers.sortedBy { it.location.distanceSquared(centerLocation) } } - /** - * Check if a container block is accessible (not blocked, etc.) - * @param block The container block to check. - * @return True if the container is accessible. - */ - fun isContainerAccessible(block: Block): Boolean { - if (block.type == Material.CHEST) { - val blockAbove = block.getRelative(BlockFace.UP) - if (blockAbove.type.isSolid && blockAbove.type.isOccluding) return false - } - return true - } - /** * Get all chunks in a bounding box (legacy method for compatibility) * @param world The world to get chunks from. From 71a1b809f93966df07a4eaf0685ba1765709d040 Mon Sep 17 00:00:00 2001 From: Illyrius Date: Tue, 11 Nov 2025 13:35:03 +0100 Subject: [PATCH 08/24] added todo Signed-off-by: Illyrius --- src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt b/src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt index 4c85b0170..faf7c4b00 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt @@ -36,5 +36,6 @@ internal object BlockUtils { * @return True if the container is accessible. */ val Block.isContainerAccessible: Boolean + // TODO: Expand this to other container types. make it generic. get() = type != Material.CHEST || !getRelative(BlockFace.UP).type.let { it.isSolid && it.isOccluding } } From 9322063ff4dcaa61a234858af1a873a156183ff1 Mon Sep 17 00:00:00 2001 From: Illyrius Date: Tue, 11 Nov 2025 13:49:25 +0100 Subject: [PATCH 09/24] refactor: simplify BookData initialization by setting default values for title and author Signed-off-by: Illyrius --- .../kotlin/org/xodium/vanillaplus/data/BookData.kt | 10 ++++++---- .../org/xodium/vanillaplus/modules/BooksModule.kt | 5 ----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/org/xodium/vanillaplus/data/BookData.kt b/src/main/kotlin/org/xodium/vanillaplus/data/BookData.kt index 5e189c722..2672cde75 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/data/BookData.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/data/BookData.kt @@ -2,21 +2,23 @@ package org.xodium.vanillaplus.data import net.kyori.adventure.inventory.Book import org.bukkit.permissions.PermissionDefault +import org.xodium.vanillaplus.VanillaPlus.Companion.instance import org.xodium.vanillaplus.utils.ExtUtils.mm +import org.xodium.vanillaplus.utils.FmtUtils.fireFmt /** * Represents the data structure for a book in the game. * @property cmd The command associated with the book. * @property permission The default permission level for this book's command (defaults to TRUE). - * @property title The [title] of the book. - * @property author The [author] of the book. + * @property title The [title] of the book. Defaults to the command name with the first letter capitalized and formatted. + * @property author The [author] of the book. Defaults to the name of the main plugin instance class. * @property pages The content of the book, represented as a list of [pages], where each page is a list of lines. */ internal data class BookData( val cmd: String, val permission: PermissionDefault = PermissionDefault.TRUE, - private val title: String, - private val author: String, + private val title: String = cmd.replaceFirstChar { it.uppercase() }.fireFmt(), + private val author: String = instance::class.simpleName.toString(), private val pages: List>, ) { /** diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/BooksModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/BooksModule.kt index 72c0556f6..70e1c7734 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/BooksModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/BooksModule.kt @@ -3,13 +3,11 @@ package org.xodium.vanillaplus.modules import io.papermc.paper.command.brigadier.Commands import org.bukkit.entity.Player import org.bukkit.permissions.Permission -import org.bukkit.permissions.PermissionDefault import org.xodium.vanillaplus.VanillaPlus.Companion.instance import org.xodium.vanillaplus.data.BookData import org.xodium.vanillaplus.data.CommandData import org.xodium.vanillaplus.interfaces.ModuleInterface import org.xodium.vanillaplus.utils.ExtUtils.tryCatch -import org.xodium.vanillaplus.utils.FmtUtils.fireFmt /** Represents a module handling book mechanics within the system. */ internal class BooksModule : ModuleInterface { @@ -49,9 +47,6 @@ internal class BooksModule : ModuleInterface { listOf( BookData( cmd = "rules", - permission = PermissionDefault.TRUE, - title = "Rules".fireFmt(), - author = instance::class.simpleName.toString(), pages = listOf( // Page 1: Player Rules (1-7) From b880d8a353abd6ffb060a6d0b54c290b6c90d315 Mon Sep 17 00:00:00 2001 From: Illyrius Date: Tue, 11 Nov 2025 14:48:17 +0100 Subject: [PATCH 10/24] refactor: enhance command utility functions for improved hover text and command execution Signed-off-by: Illyrius --- .../xodium/vanillaplus/modules/ChatModule.kt | 67 +++++++------------ .../org/xodium/vanillaplus/utils/ExtUtils.kt | 35 ++++++++-- 2 files changed, 53 insertions(+), 49 deletions(-) diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/ChatModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/ChatModule.kt index 66af3d663..2a4738bf8 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/ChatModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/ChatModule.kt @@ -18,7 +18,9 @@ import org.bukkit.permissions.PermissionDefault import org.xodium.vanillaplus.VanillaPlus.Companion.instance import org.xodium.vanillaplus.data.CommandData import org.xodium.vanillaplus.interfaces.ModuleInterface +import org.xodium.vanillaplus.utils.ExtUtils.clickOpenUrl import org.xodium.vanillaplus.utils.ExtUtils.clickRunCmd +import org.xodium.vanillaplus.utils.ExtUtils.clickSuggestCmd import org.xodium.vanillaplus.utils.ExtUtils.face import org.xodium.vanillaplus.utils.ExtUtils.mm import org.xodium.vanillaplus.utils.ExtUtils.prefix @@ -70,25 +72,6 @@ internal class ChatModule : ModuleInterface { "This command allows you to whisper to players", listOf("w", "msg", "tell", "tellraw"), ), - CommandData( - Commands - .literal("guide") - .requires { it.sender.hasPermission(perms()[1]) } - .executes { ctx -> - ctx.tryCatch { - if (it.sender !is Player) instance.logger.warning("Command can only be executed by a Player!") - val sender = it.sender as Player - sender.sendMessage( - instance.prefix + - "Click me to open url!".mangoFmt(true).mm().clickEvent( - ClickEvent.openUrl("https://illyria.fandom.com/"), - ), - ) - } - }, - "This command redirects you to the wiki", - emptyList(), - ), ) } @@ -99,11 +82,6 @@ internal class ChatModule : ModuleInterface { "Allows use of the whisper command", PermissionDefault.TRUE, ), - Permission( - "${instance::class.simpleName}.guide".lowercase(), - "Allows use of the guide command", - PermissionDefault.TRUE, - ), ) @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) @@ -139,13 +117,7 @@ internal class ChatModule : ModuleInterface { Regex("") .replace(config.welcomeText.joinToString("\n")) { "" } .mm( - Placeholder.component( - "player", - player - .displayName() - .clickEvent(ClickEvent.suggestCommand("/nickname ${player.name}")) - .hoverEvent(HoverEvent.showText(config.i18n.clickMe.mm())), - ), + Placeholder.component("player", player.displayName()), *player .face() .lines() @@ -220,15 +192,26 @@ internal class ChatModule : ModuleInterface { "]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[".mangoFmt(true), "${"⯈".mangoFmt(true)}", "${"⯈".mangoFmt(true)}", - "${"⯈".mangoFmt(true)} ${"Welcome".fireFmt()} ", - "${"⯈".mangoFmt(true)}", - "${"⯈".mangoFmt(true)}", - "${"⯈".mangoFmt(true)} ${"Check out".fireFmt()}: ${ - "/rules".clickRunCmd("Click Me!".fireFmt()).skylineFmt() - } 🟅 ${ - "/guide".clickRunCmd("Click Me!".fireFmt()).skylineFmt() + "${"⯈".mangoFmt(true)} ${"Welcome".fireFmt()} ${ + "".clickSuggestCmd( + "/nickname ", + "Set your nickname!".mangoFmt(), + ) }", "${"⯈".mangoFmt(true)}", + "${"⯈".mangoFmt(true)} ${"Check out".fireFmt()}:", + "${"⯈".mangoFmt(true)} ${ + "".clickRunCmd( + "/rules", + "View the server /rules".mangoFmt(), + ) + }", + "${"⯈".mangoFmt(true)} ${ + "".clickOpenUrl( + "https://illyria.fandom.com", + "Visit the wiki!".mangoFmt(), + ) + }", "${"⯈".mangoFmt(true)}", "]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[=]|[".mangoFmt(true), ), @@ -240,11 +223,11 @@ internal class ChatModule : ModuleInterface { var i18n: I18n = I18n(), ) : ModuleInterface.Config { data class I18n( - var clickMe: String = "Click Me!".fireFmt(), - var clickToWhisper: String = "Click to Whisper".fireFmt(), + var clickMe: String = "Click me!".mangoFmt(), + var clickToWhisper: String = "Click to Whisper".mangoFmt(), var playerIsNotOnline: String = "${instance.prefix} Player is not Online!".fireFmt(), - var deleteMessage: String = "Click to delete your message".fireFmt(), - var clickToClipboard: String = "Click to copy position to clipboard".fireFmt(), + var deleteMessage: String = "Click to delete your message".mangoFmt(), + var clickToClipboard: String = "Click to copy position to clipboard".mangoFmt(), var playerSetSpawn: String = "${"❗".fireFmt()} ${"›".mangoFmt(true)} ", ) } diff --git a/src/main/kotlin/org/xodium/vanillaplus/utils/ExtUtils.kt b/src/main/kotlin/org/xodium/vanillaplus/utils/ExtUtils.kt index 40e058a5e..dbf1b3406 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/utils/ExtUtils.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/utils/ExtUtils.kt @@ -77,15 +77,36 @@ internal object ExtUtils { /** * Performs a command from a [String]. - * @param hover Optional hover text for the command. + * @param cmd The command to perform. + * @param hover Optional hover text for the command. Defaults to "Click me!". * @return The formatted [String] with the command. */ - fun String.clickRunCmd(hover: String? = null): String = - if (hover != null) { - "$this" - } else { - "$this" - } + fun String.clickRunCmd( + cmd: String, + hover: String? = "Click me!".mangoFmt(), + ): String = "$this" + + /** + * Suggests a command from a [String]. + * @param cmd The command to suggest. + * @param hover Optional hover text for the command. Defaults to "Click me!". + * @return The formatted [String] with the suggested command. + */ + fun String.clickSuggestCmd( + cmd: String, + hover: String? = "Click me!".mangoFmt(), + ): String = "$this" + + /** + * Opens a URL from a [String]. + * @param url The URL to open. + * @param hover Optional hover text for the URL. Defaults to "Click me!". + * @return The formatted [String] with the URL. + */ + fun String.clickOpenUrl( + url: String, + hover: String? = "Click me!".mangoFmt(), + ): String = "$this" /** * A helper function to wrap command execution with standardized error handling. From e5b97780539547d33765052ee35219d43b0f11f4 Mon Sep 17 00:00:00 2001 From: Illyrius Date: Tue, 11 Nov 2025 14:50:27 +0100 Subject: [PATCH 11/24] refactor: update delete cross-component to use SignedMessage for improved message handling Signed-off-by: Illyrius --- .../kotlin/org/xodium/vanillaplus/modules/ChatModule.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/ChatModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/ChatModule.kt index 2a4738bf8..d56a0f855 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/ChatModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/ChatModule.kt @@ -5,6 +5,7 @@ import com.mojang.brigadier.arguments.StringArgumentType import io.papermc.paper.chat.ChatRenderer import io.papermc.paper.command.brigadier.Commands import io.papermc.paper.event.player.AsyncChatEvent +import net.kyori.adventure.chat.SignedMessage import net.kyori.adventure.text.Component import net.kyori.adventure.text.event.ClickEvent import net.kyori.adventure.text.event.HoverEvent @@ -102,7 +103,7 @@ internal class ChatModule : ModuleInterface { ), Placeholder.component("message", message.pt.mm()), ) - if (audience == player) base = base.appendSpace().append(createDeleteCross(event)) + if (audience == player) base = base.appendSpace().append(createDeleteCross(event.signedMessage())) base } } @@ -175,14 +176,14 @@ internal class ChatModule : ModuleInterface { /** * Creates to delete cross-component for message deletion. - * @param event The [AsyncChatEvent] containing the message to be deleted. + * @param signedMessage The signed message to be deleted. * @return A [Component] representing the delete cross with hover text and click action. */ - private fun createDeleteCross(event: AsyncChatEvent): Component = + private fun createDeleteCross(signedMessage: SignedMessage): Component = config.deleteCross .mm() .hoverEvent(config.i18n.deleteMessage.mm()) - .clickEvent(ClickEvent.callback { instance.server.deleteMessage(event.signedMessage()) }) + .clickEvent(ClickEvent.callback { instance.server.deleteMessage(signedMessage) }) data class Config( override var enabled: Boolean = true, From 8158bc0a98826ca071bbc2c019dc2a9d8b33df0e Mon Sep 17 00:00:00 2001 From: Illyrius Date: Tue, 11 Nov 2025 15:06:39 +0100 Subject: [PATCH 12/24] refactor: remove isContainerAccessible check from inventory module for cleaner code Signed-off-by: Illyrius --- .../kotlin/org/xodium/vanillaplus/modules/InvModule.kt | 3 --- .../kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt | 10 ---------- 2 files changed, 13 deletions(-) diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt index 8e5e50caf..a6fe5433f 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt @@ -29,7 +29,6 @@ import org.xodium.vanillaplus.data.SoundData import org.xodium.vanillaplus.interfaces.ModuleInterface import org.xodium.vanillaplus.registries.MaterialRegistry import org.xodium.vanillaplus.utils.BlockUtils.center -import org.xodium.vanillaplus.utils.BlockUtils.isContainerAccessible import org.xodium.vanillaplus.utils.ChunkUtils.filterAndSortContainers import org.xodium.vanillaplus.utils.ChunkUtils.findContainersInRadius import org.xodium.vanillaplus.utils.ExtUtils.mm @@ -155,7 +154,6 @@ internal class InvModule : ModuleInterface { location = player.location, radius = config.searchRadius, containerTypes = MaterialRegistry.CONTAINER_TYPES, - containerFilter = { it.isContainerAccessible }, ) val matchingContainers = containers.filter { container -> @@ -228,7 +226,6 @@ internal class InvModule : ModuleInterface { location = player.location, radius = config.unloadRadius, containerTypes = MaterialRegistry.CONTAINER_TYPES, - containerFilter = { it.isContainerAccessible }, ) val sortedChests = filterAndSortContainers(containers, player.location) diff --git a/src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt b/src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt index faf7c4b00..d4359578d 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt @@ -1,9 +1,7 @@ package org.xodium.vanillaplus.utils import org.bukkit.Location -import org.bukkit.Material import org.bukkit.block.Block -import org.bukkit.block.BlockFace import org.bukkit.block.Chest import org.bukkit.block.DoubleChest @@ -30,12 +28,4 @@ internal object BlockUtils { return Location(world, cx, cy, cz) } - - /** - * Check if a container block is accessible (not blocked from above). - * @return True if the container is accessible. - */ - val Block.isContainerAccessible: Boolean - // TODO: Expand this to other container types. make it generic. - get() = type != Material.CHEST || !getRelative(BlockFace.UP).type.let { it.isSolid && it.isOccluding } } From c2e9f3e418e7c074d0a46acf287589f759297495 Mon Sep 17 00:00:00 2001 From: Illyrius Date: Tue, 11 Nov 2025 15:10:41 +0100 Subject: [PATCH 13/24] refactor: move hasMatchingEnchantments logic to ItemStackUtils for better modularity Signed-off-by: Illyrius --- .../xodium/vanillaplus/modules/InvModule.kt | 32 ++----------------- .../vanillaplus/utils/ItemStackUtils.kt | 32 +++++++++++++++++++ 2 files changed, 35 insertions(+), 29 deletions(-) create mode 100644 src/main/kotlin/org/xodium/vanillaplus/utils/ItemStackUtils.kt diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt index a6fe5433f..9a9623433 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt @@ -6,7 +6,6 @@ import com.mojang.brigadier.arguments.StringArgumentType import com.mojang.brigadier.context.CommandContext import io.papermc.paper.command.brigadier.CommandSourceStack import io.papermc.paper.command.brigadier.Commands -import io.papermc.paper.datacomponent.DataComponentTypes import net.kyori.adventure.sound.Sound import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder import org.bukkit.Color @@ -37,6 +36,7 @@ import org.xodium.vanillaplus.utils.FmtUtils.fireFmt import org.xodium.vanillaplus.utils.FmtUtils.glorpFmt import org.xodium.vanillaplus.utils.FmtUtils.roseFmt import org.xodium.vanillaplus.utils.InvUtils.transferItems +import org.xodium.vanillaplus.utils.ItemStackUtils import java.util.* import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentHashMap @@ -158,7 +158,7 @@ internal class InvModule : ModuleInterface { val matchingContainers = containers.filter { container -> (container as Container).inventory.contents.any { item -> - item?.type == material && hasMatchingEnchantments(ItemStack(material), item) + item?.type == material && ItemStackUtils.hasMatchingEnchantments(ItemStack.of(material), item) } } @@ -285,7 +285,7 @@ internal class InvModule : ModuleInterface { startSlot = startSlot, endSlot = endSlot, onlyMatching = true, - enchantmentChecker = ::hasMatchingEnchantments, + enchantmentChecker = ItemStackUtils::hasMatchingEnchantments, ) if (success && itemsTransferred > 0) { @@ -333,36 +333,10 @@ internal class InvModule : ModuleInterface { return taskId } - /** - * Checks if two ItemStacks have matching enchantments. - * @param first The first ItemStack. - * @param second The second ItemStack. - * @return True if the enchantments match, false otherwise. - */ - @Suppress("UnstableApiUsage") - private fun hasMatchingEnchantments( - first: ItemStack, - second: ItemStack, - ): Boolean { - if (!config.matchEnchantments && (!config.matchEnchantmentsOnBooks || first.type != Material.ENCHANTED_BOOK)) return true - // Early return if both items have no enchantments - if (first.enchantments.isEmpty() && second.enchantments.isEmpty()) return true - // Gets enchantments from the ItemStack Data. - val firstEnchants = first.getData(DataComponentTypes.ENCHANTMENTS) - val secondEnchants = second.getData(DataComponentTypes.ENCHANTMENTS) - // Gets the stored enchantments from the ItemStack Data. - val firstStoredEnchants = first.getData(DataComponentTypes.STORED_ENCHANTMENTS) - val secondStoredEnchants = second.getData(DataComponentTypes.STORED_ENCHANTMENTS) - // Compares the enchantments and stored enchantments between the 2 ItemStack Data's. - return firstEnchants == secondEnchants && firstStoredEnchants == secondStoredEnchants - } - data class Config( override var enabled: Boolean = true, var searchRadius: Int = 25, var unloadRadius: Int = 25, - var matchEnchantments: Boolean = true, - var matchEnchantmentsOnBooks: Boolean = true, var soundOnUnload: SoundData = SoundData( BukkitSound.ENTITY_PLAYER_LEVELUP, diff --git a/src/main/kotlin/org/xodium/vanillaplus/utils/ItemStackUtils.kt b/src/main/kotlin/org/xodium/vanillaplus/utils/ItemStackUtils.kt new file mode 100644 index 000000000..f618ae0a0 --- /dev/null +++ b/src/main/kotlin/org/xodium/vanillaplus/utils/ItemStackUtils.kt @@ -0,0 +1,32 @@ +package org.xodium.vanillaplus.utils + +import io.papermc.paper.datacomponent.DataComponentTypes +import org.bukkit.Material +import org.bukkit.inventory.ItemStack + +/** ItemStack utilities. */ +internal object ItemStackUtils { + /** + * Checks if two ItemStacks have matching enchantments. + * @param first The first ItemStack. + * @param second The second ItemStack. + * @return True if the enchantments match, false otherwise. + */ + @Suppress("UnstableApiUsage") + fun hasMatchingEnchantments( + first: ItemStack, + second: ItemStack, + ): Boolean { + if (first.type != Material.ENCHANTED_BOOK) return true + // Early return if both items have no enchantments + if (first.enchantments.isEmpty() && second.enchantments.isEmpty()) return true + // Gets enchantments from the ItemStack Data. + val firstEnchants = first.getData(DataComponentTypes.ENCHANTMENTS) + val secondEnchants = second.getData(DataComponentTypes.ENCHANTMENTS) + // Gets the stored enchantments from the ItemStack Data. + val firstStoredEnchants = first.getData(DataComponentTypes.STORED_ENCHANTMENTS) + val secondStoredEnchants = second.getData(DataComponentTypes.STORED_ENCHANTMENTS) + // Compares the enchantments and stored enchantments between the 2 ItemStack Data's. + return firstEnchants == secondEnchants && firstStoredEnchants == secondStoredEnchants + } +} From 3486687ece427d6fdc1549500270a3c0b8bf611f Mon Sep 17 00:00:00 2001 From: Illyrius Date: Tue, 11 Nov 2025 15:37:48 +0100 Subject: [PATCH 14/24] refactor: remove lastUnloads map and simplify player quit handling Signed-off-by: Illyrius --- .../kotlin/org/xodium/vanillaplus/modules/InvModule.kt | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt index 9a9623433..25de792fa 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt @@ -47,7 +47,6 @@ internal class InvModule : ModuleInterface { override val config: Config = Config() private val unloads = ConcurrentHashMap>() - private val lastUnloads = ConcurrentHashMap>() private val activeVisualizations = ConcurrentHashMap>() override fun cmds(): List = @@ -108,10 +107,7 @@ internal class InvModule : ModuleInterface { fun on(event: PlayerQuitEvent) { if (!enabled()) return - val uuid = event.player.uniqueId - - lastUnloads.remove(uuid) - activeVisualizations.remove(uuid) + activeVisualizations.remove(event.player.uniqueId) } /** @@ -246,7 +242,6 @@ internal class InvModule : ModuleInterface { } player.sendActionBar(config.i18n.inventoryUnloaded.mm()) - lastUnloads[player.uniqueId] = affectedChests for (chest in affectedChests) { Particle.DUST From c21e37c3fa70895e9bdbfce7a73d92dc23079f04 Mon Sep 17 00:00:00 2001 From: Illyrius Date: Tue, 11 Nov 2025 15:39:32 +0100 Subject: [PATCH 15/24] woops Signed-off-by: Illyrius --- src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt b/src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt index d4359578d..cf1ec15b2 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/utils/BlockUtils.kt @@ -1,3 +1,5 @@ +@file:Suppress("unused") + package org.xodium.vanillaplus.utils import org.bukkit.Location From eb896f9a4ccff23727fb0efef4b35ef2c98f88e6 Mon Sep 17 00:00:00 2001 From: Illyrius Date: Tue, 11 Nov 2025 15:41:12 +0100 Subject: [PATCH 16/24] cleanup Signed-off-by: Illyrius --- .../xodium/vanillaplus/utils/ChunkUtils.kt | 26 ---------- .../org/xodium/vanillaplus/utils/InvUtils.kt | 52 ------------------- 2 files changed, 78 deletions(-) diff --git a/src/main/kotlin/org/xodium/vanillaplus/utils/ChunkUtils.kt b/src/main/kotlin/org/xodium/vanillaplus/utils/ChunkUtils.kt index a0981857d..69b25ae72 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/utils/ChunkUtils.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/utils/ChunkUtils.kt @@ -2,15 +2,12 @@ package org.xodium.vanillaplus.utils -import org.bukkit.Chunk import org.bukkit.Location import org.bukkit.Material -import org.bukkit.World import org.bukkit.block.Block import org.bukkit.block.Container import org.bukkit.block.DoubleChest import org.bukkit.inventory.InventoryHolder -import org.bukkit.util.BoundingBox import java.util.* import java.util.concurrent.ConcurrentHashMap @@ -113,27 +110,4 @@ internal object ChunkUtils { return filteredContainers.sortedBy { it.location.distanceSquared(centerLocation) } } - - /** - * Get all chunks in a bounding box (legacy method for compatibility) - * @param world The world to get chunks from. - * @param box The bounding box to get chunks from. - * @return List of chunks in the bounding box. - */ - fun getChunksInBox( - world: World, - box: BoundingBox, - ): List { - val minChunkX = Math.floorDiv(box.minX.toInt(), 16) - val maxChunkX = Math.floorDiv(box.maxX.toInt(), 16) - val minChunkZ = Math.floorDiv(box.minZ.toInt(), 16) - val maxChunkZ = Math.floorDiv(box.maxZ.toInt(), 16) - return mutableListOf().apply { - for (x in minChunkX..maxChunkX) { - for (z in minChunkZ..maxChunkZ) { - if (world.isChunkLoaded(x, z)) add(world.getChunkAt(x, z)) - } - } - } - } } diff --git a/src/main/kotlin/org/xodium/vanillaplus/utils/InvUtils.kt b/src/main/kotlin/org/xodium/vanillaplus/utils/InvUtils.kt index c2d51a1ce..d3f79db31 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/utils/InvUtils.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/utils/InvUtils.kt @@ -1,46 +1,12 @@ package org.xodium.vanillaplus.utils -import org.bukkit.Material import org.bukkit.Tag -import org.bukkit.block.Container import org.bukkit.block.ShulkerBox import org.bukkit.inventory.Inventory import org.bukkit.inventory.ItemStack /** Inventory utilities. */ internal object InvUtils { - /** - * Counts the total number of items in the given inventory. - * @param inventory The inventory to count items in. - * @return The total number of items in the inventory. - */ - fun countContents(inventory: Inventory): Int = inventory.contents.filterNotNull().sumOf { it.amount } - - /** - * Get the amount of a specific material in an inventory. - * @param inventory The inventory to check. - * @param material The material to count. - * @return The amount of the material in the inventory. - */ - fun getMaterialCount( - inventory: Inventory, - material: Material, - ): Int = - inventory.contents - .filter { it?.type == material } - .sumOf { it?.amount ?: 0 } - - /** - * Check if an inventory contains an item with the same type. - * @param inventory The inventory to check. - * @param item The item to check for. - * @return True if the inventory contains the item type, false otherwise. - */ - fun containsItemType( - inventory: Inventory, - item: ItemStack, - ): Boolean = inventory.contents.any { it?.type == item.type } - /** * Check if transferring an item would be valid (not putting shulker in shulker, etc.) * @param item The item to transfer. @@ -109,22 +75,4 @@ internal object InvUtils { .asSequence() .filterNotNull() .any { it.type == item.type && enchantmentChecker(item, it) } - - /** - * Search containers for specific material. - * @param containers List of containers to search. - * @param material The material to search for. - * @param enchantmentChecker Function to check enchantment compatibility. - * @return List of containers that contain the material. - */ - fun searchContainersForMaterial( - containers: List, - material: Material, - enchantmentChecker: (ItemStack, ItemStack) -> Boolean, - ): List = - containers.filter { container -> - container.inventory.contents.any { item -> - item?.type == material && enchantmentChecker(ItemStack(material), item) - } - } } From ca5eae0953fa86fa9a49fd376b6e18457081262a Mon Sep 17 00:00:00 2001 From: Illyrius Date: Tue, 11 Nov 2025 15:48:54 +0100 Subject: [PATCH 17/24] refactor: simplify item transfer logic and remove unused performUnload method Signed-off-by: Illyrius --- .../xodium/vanillaplus/modules/InvModule.kt | 49 +++++-------------- .../org/xodium/vanillaplus/utils/InvUtils.kt | 6 +-- 2 files changed, 13 insertions(+), 42 deletions(-) diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt index 25de792fa..6c71f6199 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt @@ -9,7 +9,6 @@ import io.papermc.paper.command.brigadier.Commands import net.kyori.adventure.sound.Sound import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder import org.bukkit.Color -import org.bukkit.Location import org.bukkit.Material import org.bukkit.Particle import org.bukkit.block.Block @@ -18,7 +17,6 @@ import org.bukkit.entity.Player import org.bukkit.event.EventHandler import org.bukkit.event.EventPriority import org.bukkit.event.player.PlayerQuitEvent -import org.bukkit.inventory.Inventory import org.bukkit.inventory.ItemStack import org.bukkit.permissions.Permission import org.bukkit.permissions.PermissionDefault @@ -46,7 +44,6 @@ import org.bukkit.Sound as BukkitSound internal class InvModule : ModuleInterface { override val config: Config = Config() - private val unloads = ConcurrentHashMap>() private val activeVisualizations = ConcurrentHashMap>() override fun cmds(): List = @@ -233,7 +230,17 @@ internal class InvModule : ModuleInterface { val affectedChests = mutableListOf() for (block in sortedChests) { - if (performUnload(player, (block as Container).inventory, startSlot, endSlot)) affectedChests.add(block) + val transfer = + transferItems( + source = player.inventory, + destination = (block as Container).inventory, + startSlot = startSlot, + endSlot = endSlot, + onlyMatching = true, + enchantmentChecker = ItemStackUtils::hasMatchingEnchantments, + ) + + if (transfer) affectedChests.add(block) } if (affectedChests.isEmpty()) { @@ -256,40 +263,6 @@ internal class InvModule : ModuleInterface { player.playSound(config.soundOnUnload.toSound(), Sound.Emitter.self()) } - /** - * Transfers items from the specified player's inventory to the destination inventory within the given slot range. - * - * Only items matching certain criteria (e.g., matching enchantments) are transferred. - * - * @param player The player whose inventory items are to be transferred. - * @param destination The inventory to which items will be transferred. - * @param startSlot The starting slot index in the player's inventory to consider for transfer. - * @param endSlot The ending slot index in the player's inventory to consider for transfer. - * @return `true` if any items were successfully transferred, `false` otherwise. - */ - private fun performUnload( - player: Player, - destination: Inventory, - startSlot: Int, - endSlot: Int, - ): Boolean { - val (success, itemsTransferred) = - transferItems( - source = player.inventory, - destination = destination, - startSlot = startSlot, - endSlot = endSlot, - onlyMatching = true, - enchantmentChecker = ItemStackUtils::hasMatchingEnchantments, - ) - - if (success && itemsTransferred > 0) { - destination.location?.let { location -> unloads.computeIfAbsent(location) { mutableMapOf() } } - } - - return success - } - /** * Schedules a repeating task for a specific player and automatically cancels it after a set duration. * @param player The player for whom the task is scheduled. diff --git a/src/main/kotlin/org/xodium/vanillaplus/utils/InvUtils.kt b/src/main/kotlin/org/xodium/vanillaplus/utils/InvUtils.kt index d3f79db31..3a910d592 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/utils/InvUtils.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/utils/InvUtils.kt @@ -35,9 +35,8 @@ internal object InvUtils { endSlot: Int = 35, onlyMatching: Boolean = false, enchantmentChecker: (ItemStack, ItemStack) -> Boolean = { _, _ -> true }, - ): Pair { + ): Boolean { var moved = false - var totalTransferred = 0 for (i in startSlot..endSlot) { val item = source.getItem(i) ?: continue @@ -50,13 +49,12 @@ internal object InvUtils { if (movedAmount > 0) { moved = true - totalTransferred += movedAmount source.clear(i) leftovers.values.firstOrNull()?.let { source.setItem(i, it) } } } - return Pair(moved, totalTransferred) + return moved } /** From 9aea884f9f7fe586b253ce660ba23670f1e8c8cc Mon Sep 17 00:00:00 2001 From: Illyrius Date: Tue, 11 Nov 2025 15:54:36 +0100 Subject: [PATCH 18/24] refactor: improve container handling in inventory transfer logic Signed-off-by: Illyrius --- .../kotlin/org/xodium/vanillaplus/modules/InvModule.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt index 6c71f6199..829da7402 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt @@ -149,8 +149,9 @@ internal class InvModule : ModuleInterface { containerTypes = MaterialRegistry.CONTAINER_TYPES, ) val matchingContainers = - containers.filter { container -> - (container as Container).inventory.contents.any { item -> + containers.filter { block -> + val state = block.state as? Container ?: return@filter false + state.inventory.contents.any { item -> item?.type == material && ItemStackUtils.hasMatchingEnchantments(ItemStack.of(material), item) } } @@ -230,10 +231,11 @@ internal class InvModule : ModuleInterface { val affectedChests = mutableListOf() for (block in sortedChests) { + val destination = (block.state as? Container)?.inventory ?: continue val transfer = transferItems( source = player.inventory, - destination = (block as Container).inventory, + destination = destination, startSlot = startSlot, endSlot = endSlot, onlyMatching = true, From 9b8e3c1fa94c878c764ef4296f98c99fc5ba0a4d Mon Sep 17 00:00:00 2001 From: Illyrius Date: Tue, 11 Nov 2025 15:56:49 +0100 Subject: [PATCH 19/24] add todo Signed-off-by: Illyrius --- src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt index 829da7402..5ce34b72f 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt @@ -40,6 +40,8 @@ import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentHashMap import org.bukkit.Sound as BukkitSound +// FIX: [15:55:57 WARN]: [org.bukkit.craftbukkit.legacy.CraftLegacy] Initializing Legacy Material Support. Unless you have legacy plugins and/or data this is a bug! + /** Represents a module handling inv mechanics within the system. */ internal class InvModule : ModuleInterface { override val config: Config = Config() From ccde60b4a6f493fce25bc9f602171b8cb8aa8f18 Mon Sep 17 00:00:00 2001 From: Illyrius Date: Wed, 12 Nov 2025 08:45:12 +0100 Subject: [PATCH 20/24] fix legacy material usage Signed-off-by: Illyrius --- src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt index 5ce34b72f..fe49857ff 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt @@ -40,8 +40,6 @@ import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentHashMap import org.bukkit.Sound as BukkitSound -// FIX: [15:55:57 WARN]: [org.bukkit.craftbukkit.legacy.CraftLegacy] Initializing Legacy Material Support. Unless you have legacy plugins and/or data this is a bug! - /** Represents a module handling inv mechanics within the system. */ internal class InvModule : ModuleInterface { override val config: Config = Config() @@ -118,7 +116,9 @@ internal class InvModule : ModuleInterface { val player = ctx.source.sender as? Player ?: return 0 val materialName = runCatching { StringArgumentType.getString(ctx, "material") }.getOrNull() val material = - materialName?.let { Material.getMaterial(it.uppercase()) } ?: player.inventory.itemInMainHand.type + runCatching { materialName?.let { Material.valueOf(it.uppercase(Locale.ROOT)) } } + .getOrNull() + ?: player.inventory.itemInMainHand.type if (material == Material.AIR) { player.sendActionBar(config.i18n.noMaterialSpecified.mm()) From 78103cd2ecab5db0a10f32ccdacad4aa1443cd40 Mon Sep 17 00:00:00 2001 From: Illyrius Date: Wed, 12 Nov 2025 09:28:20 +0100 Subject: [PATCH 21/24] confused Signed-off-by: Illyrius --- src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt index fe49857ff..829da7402 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/InvModule.kt @@ -116,9 +116,7 @@ internal class InvModule : ModuleInterface { val player = ctx.source.sender as? Player ?: return 0 val materialName = runCatching { StringArgumentType.getString(ctx, "material") }.getOrNull() val material = - runCatching { materialName?.let { Material.valueOf(it.uppercase(Locale.ROOT)) } } - .getOrNull() - ?: player.inventory.itemInMainHand.type + materialName?.let { Material.getMaterial(it.uppercase()) } ?: player.inventory.itemInMainHand.type if (material == Material.AIR) { player.sendActionBar(config.i18n.noMaterialSpecified.mm()) From 53391b517efa919b18e71b42312b98a382a15c2f Mon Sep 17 00:00:00 2001 From: Illyrius Date: Wed, 12 Nov 2025 12:14:11 +0100 Subject: [PATCH 22/24] feat: add agent migration state configuration and optimize item stack comparison logic Signed-off-by: Illyrius --- .idea/copilot.data.migration.agent.xml | 8 ++++++++ .idea/modules/VanillaPlus.main.iml | 1 + .idea/modules/VanillaPlus.test.iml | 1 + .../org/xodium/vanillaplus/utils/ItemStackUtils.kt | 13 ++----------- 4 files changed, 12 insertions(+), 11 deletions(-) create mode 100644 .idea/copilot.data.migration.agent.xml diff --git a/.idea/copilot.data.migration.agent.xml b/.idea/copilot.data.migration.agent.xml new file mode 100644 index 000000000..14f22c4e7 --- /dev/null +++ b/.idea/copilot.data.migration.agent.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules/VanillaPlus.main.iml b/.idea/modules/VanillaPlus.main.iml index 7a22db58e..dde4fd273 100644 --- a/.idea/modules/VanillaPlus.main.iml +++ b/.idea/modules/VanillaPlus.main.iml @@ -4,6 +4,7 @@ + PAPER MCP ADVENTURE diff --git a/.idea/modules/VanillaPlus.test.iml b/.idea/modules/VanillaPlus.test.iml index 28546c8b1..832085095 100644 --- a/.idea/modules/VanillaPlus.test.iml +++ b/.idea/modules/VanillaPlus.test.iml @@ -4,6 +4,7 @@ + PAPER MCP ADVENTURE diff --git a/src/main/kotlin/org/xodium/vanillaplus/utils/ItemStackUtils.kt b/src/main/kotlin/org/xodium/vanillaplus/utils/ItemStackUtils.kt index f618ae0a0..3671d9f2a 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/utils/ItemStackUtils.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/utils/ItemStackUtils.kt @@ -17,16 +17,7 @@ internal object ItemStackUtils { first: ItemStack, second: ItemStack, ): Boolean { - if (first.type != Material.ENCHANTED_BOOK) return true - // Early return if both items have no enchantments - if (first.enchantments.isEmpty() && second.enchantments.isEmpty()) return true - // Gets enchantments from the ItemStack Data. - val firstEnchants = first.getData(DataComponentTypes.ENCHANTMENTS) - val secondEnchants = second.getData(DataComponentTypes.ENCHANTMENTS) - // Gets the stored enchantments from the ItemStack Data. - val firstStoredEnchants = first.getData(DataComponentTypes.STORED_ENCHANTMENTS) - val secondStoredEnchants = second.getData(DataComponentTypes.STORED_ENCHANTMENTS) - // Compares the enchantments and stored enchantments between the 2 ItemStack Data's. - return firstEnchants == secondEnchants && firstStoredEnchants == secondStoredEnchants + if (first.type != Material.ENCHANTED_BOOK || (first.enchantments.isEmpty() && second.enchantments.isEmpty())) return true + return first.getData(DataComponentTypes.ENCHANTMENTS) == second.getData(DataComponentTypes.ENCHANTMENTS) } } From 0054d591e0aa60ffa06fcd9cae991d7e304f600b Mon Sep 17 00:00:00 2001 From: Illyrius <28700752+illyrius666@users.noreply.github.com> Date: Wed, 12 Nov 2025 12:22:53 +0100 Subject: [PATCH 23/24] Update src/main/kotlin/org/xodium/vanillaplus/utils/ItemStackUtils.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Illyrius <28700752+illyrius666@users.noreply.github.com> --- src/main/kotlin/org/xodium/vanillaplus/utils/ItemStackUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/org/xodium/vanillaplus/utils/ItemStackUtils.kt b/src/main/kotlin/org/xodium/vanillaplus/utils/ItemStackUtils.kt index 3671d9f2a..8dad251c1 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/utils/ItemStackUtils.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/utils/ItemStackUtils.kt @@ -17,7 +17,7 @@ internal object ItemStackUtils { first: ItemStack, second: ItemStack, ): Boolean { - if (first.type != Material.ENCHANTED_BOOK || (first.enchantments.isEmpty() && second.enchantments.isEmpty())) return true + if (first.type != Material.ENCHANTED_BOOK && (first.enchantments.isEmpty() && second.enchantments.isEmpty())) return true return first.getData(DataComponentTypes.ENCHANTMENTS) == second.getData(DataComponentTypes.ENCHANTMENTS) } } From c94d4c1c1e1db0e0a571db32b777823199f49903 Mon Sep 17 00:00:00 2001 From: Illyrius <28700752+illyrius666@users.noreply.github.com> Date: Wed, 12 Nov 2025 12:23:14 +0100 Subject: [PATCH 24/24] Update src/main/kotlin/org/xodium/vanillaplus/modules/ChatModule.kt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Illyrius <28700752+illyrius666@users.noreply.github.com> --- src/main/kotlin/org/xodium/vanillaplus/modules/ChatModule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/org/xodium/vanillaplus/modules/ChatModule.kt b/src/main/kotlin/org/xodium/vanillaplus/modules/ChatModule.kt index d56a0f855..8e16080e0 100644 --- a/src/main/kotlin/org/xodium/vanillaplus/modules/ChatModule.kt +++ b/src/main/kotlin/org/xodium/vanillaplus/modules/ChatModule.kt @@ -101,7 +101,7 @@ internal class ChatModule : ModuleInterface { .clickEvent(ClickEvent.suggestCommand("/w ${player.name} ")) .hoverEvent(HoverEvent.showText(config.i18n.clickToWhisper.mm())), ), - Placeholder.component("message", message.pt.mm()), + Placeholder.component("message", message), ) if (audience == player) base = base.appendSpace().append(createDeleteCross(event.signedMessage())) base