Merge pull request #30 from SKBotNL/master

Add tests and other changes
This commit was merged in pull request #30.
This commit is contained in:
NotAlexNoyle authored and GitHub committed 2026-03-10 12:21:15 -04:00
commit 25e395a94a
36 files changed
+2474 -779

No files matched your search

+14 -33
View File
@@ -10,15 +10,6 @@ plugins {
kotlin("jvm") version "2.1.21" // Import Kotlin JVM plugin.
}
/* ----------------------------- Source sets --------------------------- */
sourceSets {
create("dev") {
java.srcDir("src/dev/kotlin")
compileClasspath += sourceSets.main.get().output
runtimeClasspath += sourceSets.main.get().output
}
}
/* ---------------------------- Java / Kotlin -------------------------- */
java {
toolchain {
@@ -27,15 +18,7 @@ java {
}
}
kotlin {
jvmToolchain(17)
target { compilations { getByName("dev") { associateWith(target.compilations.getByName("main")) } } }
}
configurations {
named("devCompileOnly") { extendsFrom(configurations.compileOnly.get()) }
named("devRuntimeOnly") { extendsFrom(configurations.runtimeOnly.get()) }
}
kotlin { jvmToolchain(17) }
/* ----------------------------- Metadata ------------------------------ */
val commitHash =
@@ -77,8 +60,21 @@ dependencies {
implementation("it.unimi.dsi:fastutil-core:8.5.16")
implementation("org.jetbrains.kotlin:kotlin-stdlib") // Import the Kotlin standard library.
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
testImplementation(kotlin("test"))
testImplementation("org.junit.jupiter:junit-jupiter-api:6.0.3")
testImplementation("org.junit.jupiter:junit-jupiter-params:6.0.3")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:6.0.3")
testRuntimeOnly("org.junit.platform:junit-platform-launcher:6.0.3")
testImplementation("io.mockk:mockk:1.14.9")
testImplementation("org.purpurmc.purpur:purpur-api:1.19.4-R0.1-SNAPSHOT")
testImplementation("net.luckperms:api:5.5")
testImplementation("org.slf4j:slf4j-nop:2.0.17")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
}
tasks.test { useJUnitPlatform() }
/* ---------------------- Reproducible jars ---------------------------- */
tasks.withType<AbstractArchiveTask>().configureEach { // Ensure reproducible .jars
isPreserveFileTimestamps = false
@@ -114,18 +110,3 @@ spotless {
tasks.named("spotlessCheck") {
dependsOn("spotlessApply") // Run spotless before checking if spotless ran.
}
/* ----------------------------- Shadow Dev -------------------------------- */
tasks.register<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("shadowJarDev") {
from(sourceSets.main.get().output)
from(sourceSets["dev"].output)
configurations = listOf(project.configurations.runtimeClasspath.get())
isEnableRelocation = true
relocationPrefix = "${project.group}.shadow"
}
tasks.register("buildDev") {
version = "$version-dev"
dependsOn(tasks.spotlessApply, tasks.named("shadowJarDev"))
}
@@ -1,254 +0,0 @@
package net.trueog.diamondbankog
import kotlin.collections.toTypedArray
import net.trueog.diamondbankog.InventoryExtensions.countDiamondBlocks
import net.trueog.diamondbankog.InventoryExtensions.countDiamonds
import net.trueog.diamondbankog.InventoryExtensions.countShards
import org.bukkit.Material
import org.bukkit.inventory.ItemStack
object InventoryUtilsTest : Test {
override val name: String
get() = "InventoryUtilsTest"
override suspend fun runTests(): Array<TestResult> {
val testResults = mutableListOf<TestResult>()
val tests =
arrayOf(
::testShardRemoval,
::testDiamondRemoval,
::testDiamondBlockRemoval,
::testShardAndDiamondRemoval,
::testShardDiamondAndDiamondBlockRemoval,
::testShardAndDiamondRemovalWithShardChange,
::testShardDiamondAndDiamondBlockRemovalWithShardAndDiamondChange,
::testRemoveAll,
::testCountShards,
::testCountDiamonds,
::testCountDiamondBlocks,
::testNotEnoughSpaceForChange,
::testEmptyRemoveShards,
)
for (test in tests) {
val criteriaResults = test()
testResults += TestResult(test.name, criteriaResults)
}
return testResults.toTypedArray()
}
suspend fun testShardRemoval(): Array<CriteriaResult> {
val inventorySnapshot = InventorySnapshot.withContents(arrayOf(Shard.createItemStack(9)))
val removed =
InventorySnapshotUtils.removeShards(inventorySnapshot, 3).getOrElse {
val noFailureCriteria = CriteriaResult("isFailure == false", "true", false)
return arrayOf(noFailureCriteria)
}
val removedCountCriteria = CriteriaResult("removed == 3", removed.toString(), removed == 3)
val shardCount = inventorySnapshot.countShards()
val shardCountCriteria =
CriteriaResult("inventorySnapshot.countShards() == 6L", shardCount.toString(), shardCount == 6L)
return arrayOf(removedCountCriteria, shardCountCriteria)
}
suspend fun testDiamondRemoval(): Array<CriteriaResult> {
val inventorySnapshot = InventorySnapshot.withContents(arrayOf(ItemStack(Material.DIAMOND, 2)))
val removed =
InventorySnapshotUtils.removeShards(inventorySnapshot, 9).getOrElse {
val noFailureCriteria = CriteriaResult("isFailure == false", "true", false)
return arrayOf(noFailureCriteria)
}
val removedCountCriteria = CriteriaResult("removed == 9", removed.toString(), removed == 9)
val diamondCount = inventorySnapshot.countDiamonds()
val diamondCountCriteria =
CriteriaResult("inventorySnapshot.countDiamonds() == 1L", diamondCount.toString(), diamondCount == 1L)
return arrayOf(removedCountCriteria, diamondCountCriteria)
}
suspend fun testDiamondBlockRemoval(): Array<CriteriaResult> {
val inventorySnapshot = InventorySnapshot.withContents(arrayOf(ItemStack(Material.DIAMOND_BLOCK, 2)))
val removed =
InventorySnapshotUtils.removeShards(inventorySnapshot, 81).getOrElse {
val noFailureCriteria = CriteriaResult("isFailure == false", "true", false)
return arrayOf(noFailureCriteria)
}
val removedCountCriteria = CriteriaResult("removed == 81", removed.toString(), removed == 81)
val diamondBlockAmount = inventorySnapshot.countDiamondBlocks()
val diamondBlockCountCriteria =
CriteriaResult(
"inventorySnapshot.countDiamondBlocks() == 1L",
diamondBlockAmount.toString(),
diamondBlockAmount == 1L,
)
return arrayOf(removedCountCriteria, diamondBlockCountCriteria)
}
suspend fun testShardAndDiamondRemoval(): Array<CriteriaResult> {
val inventorySnapshot =
InventorySnapshot.withContents(arrayOf(Shard.createItemStack(2), ItemStack(Material.DIAMOND, 1)))
val removed =
InventorySnapshotUtils.removeShards(inventorySnapshot, 11).getOrElse {
val noFailureCriteria = CriteriaResult("isFailure == false", "true", false)
return arrayOf(noFailureCriteria)
}
val removedCountCriteria = CriteriaResult("removed == 11", removed.toString(), removed == 11)
val noContentsCriteria =
CriteriaResult(
"inventorySnapshot.all { it == null }",
inventorySnapshot.contents.joinToString(","),
inventorySnapshot.all { it == null },
)
return arrayOf(removedCountCriteria, noContentsCriteria)
}
suspend fun testShardDiamondAndDiamondBlockRemoval(): Array<CriteriaResult> {
val inventorySnapshot =
InventorySnapshot.withContents(
arrayOf(Shard.createItemStack(2), ItemStack(Material.DIAMOND, 1), ItemStack(Material.DIAMOND_BLOCK, 1))
)
val removed =
InventorySnapshotUtils.removeShards(inventorySnapshot, 92).getOrElse {
val noFailureCriteria = CriteriaResult("isFailure == false", "true", false)
return arrayOf(noFailureCriteria)
}
val removedCountCriteria = CriteriaResult("removed == 92", removed.toString(), removed == 92)
val noContentsCriteria =
CriteriaResult(
"inventorySnapshot.all { it == null }",
inventorySnapshot.contents.joinToString(","),
inventorySnapshot.all { it == null },
)
return arrayOf(removedCountCriteria, noContentsCriteria)
}
suspend fun testShardAndDiamondRemovalWithShardChange(): Array<CriteriaResult> {
val inventorySnapshot =
InventorySnapshot.withContents(arrayOf(Shard.createItemStack(2), ItemStack(Material.DIAMOND, 1)))
val removed =
InventorySnapshotUtils.removeShards(inventorySnapshot, 10).getOrElse {
val noFailureCriteria = CriteriaResult("isFailure == false", "true", false)
return arrayOf(noFailureCriteria)
}
val removedCountCriteria = CriteriaResult("removed == 10", removed.toString(), removed == 10)
val shardCount = inventorySnapshot.countShards()
val oneShardCriteria =
CriteriaResult("inventorySnapshot.countShards() == 1", shardCount.toString(), shardCount == 1L)
val diamondCount = inventorySnapshot.countDiamonds()
val noDiamondsCriteria =
CriteriaResult("inventorySnapshot.countDiamonds() == 0", diamondCount.toString(), diamondCount == 0L)
val diamondBlockCount = inventorySnapshot.countDiamondBlocks()
val noDiamondBlocksCriteria =
CriteriaResult(
"inventorySnapshot.countDiamondBlocks() == 0",
diamondBlockCount.toString(),
diamondBlockCount == 0L,
)
return arrayOf(removedCountCriteria, oneShardCriteria, noDiamondsCriteria, noDiamondBlocksCriteria)
}
suspend fun testShardDiamondAndDiamondBlockRemovalWithShardAndDiamondChange(): Array<CriteriaResult> {
val inventorySnapshot =
InventorySnapshot.withContents(
arrayOf(Shard.createItemStack(2), ItemStack(Material.DIAMOND, 1), ItemStack(Material.DIAMOND_BLOCK, 1))
)
val removed =
InventorySnapshotUtils.removeShards(inventorySnapshot, 12).getOrElse {
val noFailureCriteria = CriteriaResult("isFailure == false", "true", false)
return arrayOf(noFailureCriteria)
}
val removedCountCriteria = CriteriaResult("removed == 12", removed.toString(), removed == 12)
val shardCount = inventorySnapshot.countShards()
val oneShardCriteria =
CriteriaResult("inventorySnapshot.countShards() == 8", shardCount.toString(), shardCount == 8L)
val diamondCount = inventorySnapshot.countDiamonds()
val noDiamondsCriteria =
CriteriaResult("inventorySnapshot.countDiamonds() == 8", diamondCount.toString(), diamondCount == 8L)
val diamondBlockCount = inventorySnapshot.countDiamondBlocks()
val noDiamondBlocksCriteria =
CriteriaResult(
"inventorySnapshot.countDiamondBlocks() == 0",
diamondBlockCount.toString(),
diamondBlockCount == 0L,
)
return arrayOf(removedCountCriteria, oneShardCriteria, noDiamondsCriteria, noDiamondBlocksCriteria)
}
suspend fun testRemoveAll(): Array<CriteriaResult> {
val inventorySnapshot =
InventorySnapshot.withContents(
arrayOf(Shard.createItemStack(2), ItemStack(Material.DIAMOND, 1), ItemStack(Material.DIAMOND_BLOCK, 1))
)
val removed = InventorySnapshotUtils.removeAll(inventorySnapshot)
val removedCountCriteria = CriteriaResult("removed == 92", removed.toString(), removed == 92)
val noContentsCriteria =
CriteriaResult(
"inventorySnapshot.all { it == null }",
inventorySnapshot.contents.joinToString(","),
inventorySnapshot.all { it == null },
)
return arrayOf(removedCountCriteria, noContentsCriteria)
}
suspend fun testCountShards(): Array<CriteriaResult> {
val inventorySnapshot =
InventorySnapshot.withContents(
arrayOf(Shard.createItemStack(2), Shard.createItemStack(5), Shard.createItemStack(1))
)
val count = InventorySnapshotUtils.countShards(inventorySnapshot)
val countCriteria = CriteriaResult("count == 8", count.toString(), count == 8)
return arrayOf(countCriteria)
}
suspend fun testCountDiamonds(): Array<CriteriaResult> {
val inventorySnapshot =
InventorySnapshot.withContents(
arrayOf(ItemStack(Material.DIAMOND, 2), ItemStack(Material.DIAMOND, 5), ItemStack(Material.DIAMOND, 1))
)
val count = InventorySnapshotUtils.countDiamonds(inventorySnapshot)
val countCriteria = CriteriaResult("count == 8", count.toString(), count == 8)
return arrayOf(countCriteria)
}
suspend fun testCountDiamondBlocks(): Array<CriteriaResult> {
val inventorySnapshot =
InventorySnapshot.withContents(
arrayOf(
ItemStack(Material.DIAMOND_BLOCK, 2),
ItemStack(Material.DIAMOND_BLOCK, 5),
ItemStack(Material.DIAMOND_BLOCK, 1),
)
)
val count = InventorySnapshotUtils.countDiamondBlocks(inventorySnapshot)
val countCriteria = CriteriaResult("count == 8", count.toString(), count == 8)
return arrayOf(countCriteria)
}
suspend fun testNotEnoughSpaceForChange(): Array<CriteriaResult> {
val contents = mutableListOf<ItemStack?>()
repeat(35) { contents.add(ItemStack(Material.DIRT)) }
contents.add(ItemStack(Material.DIAMOND_BLOCK))
val inventorySnapshot = InventorySnapshot.withContents(contents.toTypedArray())
val result = InventorySnapshotUtils.removeShards(inventorySnapshot, 1)
val failureCriteria = CriteriaResult("isFailure == true", result.isFailure.toString(), result.isFailure)
return arrayOf(failureCriteria)
}
suspend fun testEmptyRemoveShards(): Array<CriteriaResult> {
val contents = emptyArray<ItemStack?>()
val inventorySnapshot = InventorySnapshot.withContents(contents)
val removed =
InventorySnapshotUtils.removeShards(inventorySnapshot, 10).getOrElse {
val noFailureCriteria = CriteriaResult("isFailure == false", "true", false)
return arrayOf(noFailureCriteria)
}
val removedCriteriaResult = CriteriaResult("removed == 0", removed.toString(), removed == 0)
return arrayOf(removedCriteriaResult)
}
}
@@ -1,7 +0,0 @@
package net.trueog.diamondbankog
interface Test {
val name: String
suspend fun runTests(): Array<TestResult>
}
@@ -1,26 +0,0 @@
package net.trueog.diamondbankog
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.kyori.adventure.text.minimessage.MiniMessage
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
internal class TestCommand : CommandExecutor {
val tests: Array<Test> = arrayOf(InventoryUtilsTest)
@OptIn(DelicateCoroutinesApi::class)
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>): Boolean {
GlobalScope.launch {
for (test in tests) {
sender.sendMessage("${test.name}:")
for (result in test.runTests()) {
sender.sendMessage(MiniMessage.miniMessage().deserialize(" $result"))
}
}
}
return true
}
}
@@ -1,11 +0,0 @@
package net.trueog.diamondbankog
import org.bukkit.plugin.java.JavaPlugin
object TestCommands {
@JvmStatic
fun register(plugin: JavaPlugin) {
plugin.getCommand("dbogtests")?.setExecutor(TestCommand())
plugin.logger.warning("Registered test commands")
}
}
@@ -1,32 +0,0 @@
package net.trueog.diamondbankog
data class CriteriaResult(val criteria: String, val value: String, val passed: Boolean)
data class TestResult(val name: String, val criteriaResults: Array<CriteriaResult>) {
override fun toString(): String {
val passed = criteriaResults.all { it.passed }
return "${if (passed) "<green>" else "<red>"}Test \"$name\" ${if (passed) "passed" else "failed: ${
criteriaResults.filter { !it.passed }.joinToString(
", "
) { "criteria \"${it.criteria}\" not met (was \"${it.value}\")" }
}"}"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as TestResult
if (name != other.name) return false
if (!criteriaResults.contentEquals(other.criteriaResults)) return false
return true
}
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + criteriaResults.contentHashCode()
return result
}
}
@@ -34,7 +34,7 @@ internal object AutoCompress {
transactionLock.withLockSuspend(player.uniqueId) {
val inventorySnapshot = runOnMainThread {
player.inventory.lock()
InventorySnapshot.from(player.inventory)
InventorySnapshot.from(player.inventory, DiamondBankOG.balanceManager)
}
val shardsInInventory = inventorySnapshot.countShards().toInt()
@@ -2,146 +2,35 @@ package net.trueog.diamondbankog
import java.sql.SQLException
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger
import net.trueog.diamondbankog.DiamondBankException.EconomyDisabledException
import net.trueog.diamondbankog.DiamondBankException.InvalidArgumentException
import net.trueog.diamondbankog.DiamondBankOG.Companion.economyDisabled
import net.trueog.diamondbankog.ErrorHandler.handleError
import net.trueog.diamondbankog.PostgreSQL.PlayerShards
import net.trueog.diamondbankog.PostgreSQL.ShardType
internal class BalanceManager {
val cache = Cache()
val postgreSQL = PostgreSQL()
val beingModified = ConcurrentHashMap<Pair<UUID, ShardType>, AtomicInteger>()
interface BalanceManager {
@Throws(SQLException::class, ClassNotFoundException::class) fun init()
@Throws(SQLException::class, ClassNotFoundException::class)
fun init() {
postgreSQL.initDB()
}
suspend fun setPlayerShards(uuid: UUID, shards: Long, type: ShardType): Result<Unit>
private fun increment(uuid: UUID, type: ShardType) {
beingModified.computeIfAbsent(uuid to type) { AtomicInteger(0) }.incrementAndGet()
}
suspend fun addToBankShards(uuid: UUID, shards: Long): Result<Unit>
private fun decrement(uuid: UUID, type: ShardType) {
beingModified.computeIfPresent(uuid to type) { _, counter ->
val newValue = counter.decrementAndGet()
if (newValue == 0) null else counter
}
}
suspend fun subtractFromBankShards(uuid: UUID, shards: Long): Result<Unit>
suspend fun setPlayerShards(uuid: UUID, shards: Long, type: ShardType): Result<Unit> {
if (type == ShardType.TOTAL) return Result.failure(InvalidArgumentException())
if (economyDisabled) return Result.failure(EconomyDisabledException())
suspend fun getBankShards(uuid: UUID): Result<Long>
increment(uuid, type)
postgreSQL.setPlayerShards(uuid, shards, type).getOrElse {
return Result.failure(it)
}
cache.setBalance(uuid, shards, type)
decrement(uuid, type)
return Result.success(Unit)
}
suspend fun getInventoryShards(uuid: UUID): Result<Long>
private suspend fun addToPlayerShards(uuid: UUID, shards: Long, type: ShardType): Result<Unit> {
if (type == ShardType.TOTAL) return Result.failure(InvalidArgumentException())
if (economyDisabled) return Result.failure(EconomyDisabledException())
suspend fun getEnderChestShards(uuid: UUID): Result<Long>
increment(uuid, type)
val newBalanceDb =
postgreSQL.addToPlayerShards(uuid, shards, type).getOrElse {
return Result.failure(it)
}
val newBalanceCache =
cache.addBalance(uuid, shards, type).getOrElse {
return Result.failure(it)
}
if (newBalanceDb != newBalanceCache) {
handleError(IllegalStateException("Database and cache balances do not match"))
}
decrement(uuid, type)
return Result.success(Unit)
}
suspend fun getShardTypeShards(uuid: UUID, type: ShardType): Result<Long>
suspend fun addToBankShards(uuid: UUID, shards: Long): Result<Unit> =
addToPlayerShards(uuid, shards, ShardType.BANK)
suspend fun getTotalShards(uuid: UUID): Result<Long>
suspend fun subtractFromBankShards(uuid: UUID, shards: Long): Result<Unit> =
addToPlayerShards(uuid, -shards, ShardType.BANK)
suspend fun getAllShards(uuid: UUID): Result<PlayerShards>
suspend fun getBankShards(uuid: UUID) = getShardTypeShards(uuid, ShardType.BANK)
suspend fun getBaltop(offset: Int): Result<Map<UUID?, Long>>
suspend fun getInventoryShards(uuid: UUID) = getShardTypeShards(uuid, ShardType.INVENTORY)
suspend fun getBaltopWithUuid(uuid: UUID): Result<Pair<Map<UUID?, Long>, Long>>
suspend fun getEnderChestShards(uuid: UUID) = getShardTypeShards(uuid, ShardType.ENDER_CHEST)
suspend fun getShardTypeShards(uuid: UUID, type: ShardType): Result<Long> {
if ((beingModified[uuid to type]?.get() ?: 0) > 0) {
return postgreSQL.getShardTypeShards(uuid, type)
}
val cacheBalance = cache.getBalance(uuid, type)
if (cacheBalance == -1L) {
increment(uuid, type)
val dbBalance =
postgreSQL.getShardTypeShards(uuid, type).getOrElse {
return Result.failure(it)
}
cache.setBalance(uuid, dbBalance, type)
decrement(uuid, type)
return Result.success(dbBalance)
}
return Result.success(cacheBalance)
}
suspend fun getTotalShards(uuid: UUID): Result<Long> {
val anyBeingModified = beingModified.any { it.key.first == uuid && it.value.get() > 0 }
if (anyBeingModified) {
return postgreSQL.getTotalShards(uuid)
}
val cacheBalance = cache.getBalance(uuid, ShardType.TOTAL)
if (cacheBalance == -1L) {
increment(uuid, ShardType.TOTAL)
val dbBalance =
postgreSQL.getTotalShards(uuid).getOrElse {
return Result.failure(it)
}
cache.setBalance(uuid, dbBalance, ShardType.TOTAL)
decrement(uuid, ShardType.TOTAL)
return Result.success(dbBalance)
}
return Result.success(cacheBalance)
}
suspend fun getAllShards(uuid: UUID): Result<PlayerShards> {
val anyBeingModified = beingModified.any { it.key.first == uuid && it.value.get() > 0 }
if (anyBeingModified) {
return postgreSQL.getAllShards(uuid)
}
val cacheBankBalance = cache.getBalance(uuid, ShardType.BANK)
val cacheInventoryBalance = cache.getBalance(uuid, ShardType.INVENTORY)
val cacheEnderChestBalance = cache.getBalance(uuid, ShardType.ENDER_CHEST)
if (cacheBankBalance == -1L || cacheInventoryBalance == -1L || cacheEnderChestBalance == -1L) {
increment(uuid, ShardType.TOTAL)
val dbPlayerShards =
postgreSQL.getAllShards(uuid).getOrElse {
return Result.failure(it)
}
cache.setBalance(uuid, dbPlayerShards.bank, ShardType.BANK)
cache.setBalance(uuid, dbPlayerShards.inventory, ShardType.INVENTORY)
cache.setBalance(uuid, dbPlayerShards.enderChest, ShardType.ENDER_CHEST)
decrement(uuid, ShardType.TOTAL)
return Result.success(dbPlayerShards)
}
return Result.success(PlayerShards(cacheBankBalance, cacheInventoryBalance, cacheEnderChestBalance))
}
suspend fun getBaltop(offset: Int): Result<Map<UUID?, Long>> = postgreSQL.getBaltop(offset)
suspend fun getBaltopWithUuid(uuid: UUID): Result<Pair<Map<UUID?, Long>, Long>> = postgreSQL.getBaltopWithUuid(uuid)
suspend fun getNumberOfRows(): Result<Long> = postgreSQL.getNumberOfRows()
suspend fun getNumberOfRows(): Result<Long>
suspend fun insertTransactionLog(
playerUuid: UUID,
@@ -149,27 +38,13 @@ internal class BalanceManager {
playerToUuid: UUID?,
transactionReason: String,
notes: String?,
): Result<Unit> =
postgreSQL.insertTransactionLog(playerUuid, transferredShards, playerToUuid, transactionReason, notes)
): Result<Unit>
suspend fun hasEntry(uuid: UUID) = postgreSQL.hasEntry(uuid)
suspend fun hasEntry(uuid: UUID): Result<Boolean>
suspend fun cacheForPlayer(uuid: UUID): Result<Unit> {
val playerShards =
postgreSQL.getAllShards(uuid).getOrElse {
return Result.failure(it)
}
cache.setBalance(uuid, playerShards.bank, ShardType.BANK)
cache.setBalance(uuid, playerShards.inventory, ShardType.INVENTORY)
cache.setBalance(uuid, playerShards.enderChest, ShardType.ENDER_CHEST)
return Result.success(Unit)
}
suspend fun cacheForPlayer(uuid: UUID): Result<Unit>
fun removeCacheForPlayer(uuid: UUID) {
cache.removeAll(uuid)
}
fun removeCacheForPlayer(uuid: UUID)
fun shutdown() {
postgreSQL.pool.disconnect().get()
}
fun shutdown()
}
@@ -0,0 +1,176 @@
package net.trueog.diamondbankog
import java.sql.SQLException
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger
import net.trueog.diamondbankog.DiamondBankException.EconomyDisabledException
import net.trueog.diamondbankog.DiamondBankException.InvalidArgumentException
import net.trueog.diamondbankog.DiamondBankOG.Companion.economyDisabled
import net.trueog.diamondbankog.ErrorHandler.handleError
import net.trueog.diamondbankog.PostgreSQL.PlayerShards
import net.trueog.diamondbankog.PostgreSQL.ShardType
internal class CachingBalanceManager : BalanceManager {
val cache = Cache()
val postgreSQL = PostgreSQL()
val beingModified = ConcurrentHashMap<Pair<UUID, ShardType>, AtomicInteger>()
@Throws(SQLException::class, ClassNotFoundException::class)
override fun init() {
postgreSQL.initDB()
}
private fun increment(uuid: UUID, type: ShardType) {
beingModified.computeIfAbsent(uuid to type) { AtomicInteger(0) }.incrementAndGet()
}
private fun decrement(uuid: UUID, type: ShardType) {
beingModified.computeIfPresent(uuid to type) { _, counter ->
val newValue = counter.decrementAndGet()
if (newValue == 0) null else counter
}
}
override suspend fun setPlayerShards(uuid: UUID, shards: Long, type: ShardType): Result<Unit> {
if (type == ShardType.TOTAL) return Result.failure(InvalidArgumentException())
if (economyDisabled) return Result.failure(EconomyDisabledException())
increment(uuid, type)
postgreSQL.setPlayerShards(uuid, shards, type).getOrElse {
return Result.failure(it)
}
cache.setBalance(uuid, shards, type)
decrement(uuid, type)
return Result.success(Unit)
}
private suspend fun addToPlayerShards(uuid: UUID, shards: Long, type: ShardType): Result<Unit> {
if (type == ShardType.TOTAL) return Result.failure(InvalidArgumentException())
if (economyDisabled) return Result.failure(EconomyDisabledException())
increment(uuid, type)
val newBalanceDb =
postgreSQL.addToPlayerShards(uuid, shards, type).getOrElse {
return Result.failure(it)
}
val newBalanceCache =
cache.addBalance(uuid, shards, type).getOrElse {
return Result.failure(it)
}
if (newBalanceDb != newBalanceCache) {
handleError(IllegalStateException("Database and cache balances do not match"))
}
decrement(uuid, type)
return Result.success(Unit)
}
override suspend fun addToBankShards(uuid: UUID, shards: Long): Result<Unit> =
addToPlayerShards(uuid, shards, ShardType.BANK)
override suspend fun subtractFromBankShards(uuid: UUID, shards: Long): Result<Unit> =
addToPlayerShards(uuid, -shards, ShardType.BANK)
override suspend fun getBankShards(uuid: UUID): Result<Long> = getShardTypeShards(uuid, ShardType.BANK)
override suspend fun getInventoryShards(uuid: UUID): Result<Long> = getShardTypeShards(uuid, ShardType.INVENTORY)
override suspend fun getEnderChestShards(uuid: UUID): Result<Long> = getShardTypeShards(uuid, ShardType.ENDER_CHEST)
override suspend fun getShardTypeShards(uuid: UUID, type: ShardType): Result<Long> {
if ((beingModified[uuid to type]?.get() ?: 0) > 0) {
return postgreSQL.getShardTypeShards(uuid, type)
}
val cacheBalance = cache.getBalance(uuid, type)
if (cacheBalance == -1L) {
increment(uuid, type)
val dbBalance =
postgreSQL.getShardTypeShards(uuid, type).getOrElse {
return Result.failure(it)
}
cache.setBalance(uuid, dbBalance, type)
decrement(uuid, type)
return Result.success(dbBalance)
}
return Result.success(cacheBalance)
}
override suspend fun getTotalShards(uuid: UUID): Result<Long> {
val anyBeingModified = beingModified.any { it.key.first == uuid && it.value.get() > 0 }
if (anyBeingModified) {
return postgreSQL.getTotalShards(uuid)
}
val cacheBalance = cache.getBalance(uuid, ShardType.TOTAL)
if (cacheBalance == -1L) {
increment(uuid, ShardType.TOTAL)
val dbBalance =
postgreSQL.getTotalShards(uuid).getOrElse {
return Result.failure(it)
}
cache.setBalance(uuid, dbBalance, ShardType.TOTAL)
decrement(uuid, ShardType.TOTAL)
return Result.success(dbBalance)
}
return Result.success(cacheBalance)
}
override suspend fun getAllShards(uuid: UUID): Result<PlayerShards> {
val anyBeingModified = beingModified.any { it.key.first == uuid && it.value.get() > 0 }
if (anyBeingModified) {
return postgreSQL.getAllShards(uuid)
}
val cacheBankBalance = cache.getBalance(uuid, ShardType.BANK)
val cacheInventoryBalance = cache.getBalance(uuid, ShardType.INVENTORY)
val cacheEnderChestBalance = cache.getBalance(uuid, ShardType.ENDER_CHEST)
if (cacheBankBalance == -1L || cacheInventoryBalance == -1L || cacheEnderChestBalance == -1L) {
increment(uuid, ShardType.TOTAL)
val dbPlayerShards =
postgreSQL.getAllShards(uuid).getOrElse {
return Result.failure(it)
}
cache.setBalance(uuid, dbPlayerShards.bank, ShardType.BANK)
cache.setBalance(uuid, dbPlayerShards.inventory, ShardType.INVENTORY)
cache.setBalance(uuid, dbPlayerShards.enderChest, ShardType.ENDER_CHEST)
decrement(uuid, ShardType.TOTAL)
return Result.success(dbPlayerShards)
}
return Result.success(PlayerShards(cacheBankBalance, cacheInventoryBalance, cacheEnderChestBalance))
}
override suspend fun getBaltop(offset: Int): Result<Map<UUID?, Long>> = postgreSQL.getBaltop(offset)
override suspend fun getBaltopWithUuid(uuid: UUID): Result<Pair<Map<UUID?, Long>, Long>> =
postgreSQL.getBaltopWithUuid(uuid)
override suspend fun getNumberOfRows(): Result<Long> = postgreSQL.getNumberOfRows()
override suspend fun insertTransactionLog(
playerUuid: UUID,
transferredShards: Long,
playerToUuid: UUID?,
transactionReason: String,
notes: String?,
): Result<Unit> =
postgreSQL.insertTransactionLog(playerUuid, transferredShards, playerToUuid, transactionReason, notes)
override suspend fun hasEntry(uuid: UUID): Result<Boolean> = postgreSQL.hasEntry(uuid)
override suspend fun cacheForPlayer(uuid: UUID): Result<Unit> {
val playerShards =
postgreSQL.getAllShards(uuid).getOrElse {
return Result.failure(it)
}
cache.setBalance(uuid, playerShards.bank, ShardType.BANK)
cache.setBalance(uuid, playerShards.inventory, ShardType.INVENTORY)
cache.setBalance(uuid, playerShards.enderChest, ShardType.ENDER_CHEST)
return Result.success(Unit)
}
override fun removeCacheForPlayer(uuid: UUID) {
cache.removeAll(uuid)
}
override fun shutdown() {
postgreSQL.pool.disconnect().get()
}
}
@@ -2,14 +2,21 @@ package net.trueog.diamondbankog
import java.util.*
import kotlin.math.floor
import net.kyori.adventure.text.minimessage.MiniMessage
import net.trueog.diamondbankog.DiamondBankException.InsufficientFundsException
import net.trueog.diamondbankog.DiamondBankOG.Companion.balanceManager
import net.trueog.diamondbankog.DiamondBankRuntimeException.MoreThanOneDecimalDigitRuntimeException
import org.bukkit.Bukkit
import org.bukkit.OfflinePlayer
object CommonOperations {
suspend fun consume(uuid: UUID, shards: Long, inventorySnapshot: InventorySnapshot): Result<Unit> {
suspend fun consume(
uuid: UUID,
shards: Long,
inventorySnapshot: InventorySnapshot,
config: Config = DiamondBankOG.config,
balanceManager: BalanceManager = DiamondBankOG.balanceManager,
mm: MiniMessage = DiamondBankOG.mm,
): Result<Unit> {
val bankShards =
balanceManager.getBankShards(uuid).getOrElse {
return Result.failure(it)
@@ -18,7 +25,7 @@ object CommonOperations {
if (bankShards < shards) {
val toRemoveShards = shards - bankShards
val removedInShards =
InventorySnapshotUtils.removeShards(inventorySnapshot, toRemoveShards)
InventorySnapshotUtils.removeShards(inventorySnapshot, toRemoveShards, config, balanceManager, mm)
.getOrElse {
return Result.failure(it)
}
@@ -1,88 +1,12 @@
package net.trueog.diamondbankog
import java.io.File
import kotlin.properties.Delegates
import net.trueog.diamondbankog.DiamondBankOG.Companion.plugin
import org.bukkit.configuration.file.YamlConfiguration
internal class Config private constructor() {
lateinit var prefix: String
lateinit var postgresUrl: String
lateinit var postgresUser: String
lateinit var postgresPassword: String
lateinit var postgresTable: String
lateinit var postgresLogTable: String
var postgresLogLimit by Delegates.notNull<Int>()
lateinit var redisUrl: String
companion object {
fun create(): Config? {
val config = Config()
plugin.saveDefaultConfig()
val file = File(plugin.dataFolder, "config.yml")
val yamlConfig = YamlConfiguration.loadConfiguration(file)
try {
config.prefix = yamlConfig.get("prefix") as String
} catch (_: Exception) {
plugin.logger.severe("Failed to parse config option \"prefix\" as a string")
return null
}
try {
config.postgresUrl = yamlConfig.get("postgresUrl") as String
} catch (_: Exception) {
plugin.logger.severe("Failed to parse config option \"postgresUrl\" as a string")
return null
}
try {
config.postgresUser = yamlConfig.get("postgresUser") as String
} catch (_: Exception) {
plugin.logger.severe("Failed to parse config option \"postgresUser\" as a string")
return null
}
try {
config.postgresPassword = yamlConfig.get("postgresPassword") as String
} catch (_: Exception) {
plugin.logger.severe("Failed to parse config option \"postgresPassword\" as a string")
return null
}
try {
config.postgresTable = yamlConfig.get("postgresTable") as String
} catch (_: Exception) {
plugin.logger.severe("Failed to parse config option \"postgresTable\" as a string")
return null
}
try {
config.postgresLogTable = yamlConfig.get("postgresLogTable") as String
} catch (_: Exception) {
plugin.logger.severe("Failed to parse config option \"postgresLogTable\" as a string")
return null
}
try {
config.postgresLogLimit = yamlConfig.get("postgresLogLimit") as Int
if (config.postgresLogLimit < 1) {
plugin.logger.severe("\"postgresLogLimit\" cannot be less than 1")
return null
}
} catch (_: Exception) {
plugin.logger.severe("Failed to parse config option \"postgresLogLimit\" as an integer")
return null
}
try {
config.redisUrl = yamlConfig.get("redisUrl") as String
} catch (_: Exception) {
plugin.logger.severe("Failed to parse config option \"redisUrl\" as a string")
return null
}
return config
}
}
interface Config {
var prefix: String
var postgresUrl: String
var postgresUser: String
var postgresPassword: String
var postgresTable: String
var postgresLogTable: String
var postgresLogLimit: Int
var redisUrl: String
}
@@ -0,0 +1,5 @@
package net.trueog.diamondbankog
interface ConfigFactory {
fun create(): Config?
}
@@ -19,9 +19,9 @@ internal open class DiamondBankOG : JavaPlugin() {
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
lateinit var plugin: DiamondBankOG
lateinit var config: Config
lateinit var config: YamlConfig
lateinit var eventManager: EventManager
lateinit var balanceManager: BalanceManager
lateinit var balanceManager: CachingBalanceManager
lateinit var redis: Redis
lateinit var luckPerms: LuckPerms
@@ -49,7 +49,7 @@ internal open class DiamondBankOG : JavaPlugin() {
plugin = this
Companion.config =
Config.create()
YamlConfig.create()
?: run {
Bukkit.getPluginManager().disablePlugin(this)
return
@@ -57,7 +57,7 @@ internal open class DiamondBankOG : JavaPlugin() {
eventManager = EventManager()
balanceManager = BalanceManager()
balanceManager = CachingBalanceManager()
try {
balanceManager.init()
} catch (e: Exception) {
@@ -102,12 +102,6 @@ internal open class DiamondBankOG : JavaPlugin() {
this.getCommand("enableeconomy")?.setExecutor(EnableEconomy())
this.getCommand("disableeconomy")?.setExecutor(DisableEconomy())
try {
val devCommandsClass = Class.forName("net.trueog.diamondbankog.TestCommands")
val registerMethod = devCommandsClass.getMethod("register", JavaPlugin::class.java)
registerMethod.invoke(null, this)
} catch (_: ClassNotFoundException) {}
Shard.createCraftingRecipes()
val diamondBankAPIJava = DiamondBankAPIJava()
@@ -3,7 +3,6 @@ package net.trueog.diamondbankog
import java.util.*
import java.util.function.Consumer
import kotlinx.coroutines.launch
import net.trueog.diamondbankog.DiamondBankOG.Companion.balanceManager
import net.trueog.diamondbankog.DiamondBankOG.Companion.scope
import net.trueog.diamondbankog.ErrorHandler.handleError
import net.trueog.diamondbankog.InventoryExtensions.countTotal
@@ -16,10 +15,15 @@ import org.bukkit.inventory.ItemStack
import org.bukkit.inventory.PlayerInventory
class InventorySnapshot
private constructor(private val original: Inventory, private val heldItemSlot: Int, val holder: UUID) :
Inventory by original {
private constructor(
private val original: Inventory,
private val heldItemSlot: Int,
val holder: UUID,
val balanceManager: BalanceManager,
) : Inventory by original {
companion object {
fun from(inventory: PlayerInventory): InventorySnapshot {
fun from(inventory: PlayerInventory, balanceManager: BalanceManager): InventorySnapshot {
if (!Bukkit.isPrimaryThread()) {
throw IllegalStateException("This method should only be called on the main thread")
}
@@ -28,13 +32,12 @@ private constructor(private val original: Inventory, private val heldItemSlot: I
}
val clonedInventory = Bukkit.createInventory(null, 36)
clonedInventory.contents = inventory.contents.map { it?.clone() }.toTypedArray()
return InventorySnapshot(clonedInventory, inventory.heldItemSlot, inventory.holder!!.uniqueId)
}
fun withContents(contents: Array<ItemStack?>): InventorySnapshot {
val clonedInventory = Bukkit.createInventory(null, 36)
clonedInventory.contents = contents.map { it?.clone() }.toTypedArray()
return InventorySnapshot(clonedInventory, 0, UUID.fromString("00000000-0000-0000-0000-000000000000"))
return InventorySnapshot(
clonedInventory,
inventory.heldItemSlot,
inventory.holder!!.uniqueId,
balanceManager,
)
}
}
@@ -1,16 +1,20 @@
package net.trueog.diamondbankog
import kotlin.math.ceil
import net.trueog.diamondbankog.DiamondBankOG.Companion.balanceManager
import net.trueog.diamondbankog.DiamondBankOG.Companion.config
import net.trueog.diamondbankog.DiamondBankOG.Companion.mm
import net.kyori.adventure.text.minimessage.MiniMessage
import org.bukkit.Bukkit
import org.bukkit.Material
import org.bukkit.inventory.ItemStack
object InventorySnapshotUtils {
/** @return the amount of shards that could be removed */
suspend fun removeShards(inventory: InventorySnapshot, shards: Long): Result<Int> {
suspend fun removeShards(
inventory: InventorySnapshot,
shards: Long,
config: Config,
balanceManager: BalanceManager,
mm: MiniMessage,
): Result<Int> {
require(shards > 0)
val shards = shards.toInt()
@@ -25,7 +29,7 @@ object InventorySnapshotUtils {
inventory.removeItem(ItemStack(Material.DIAMOND, diamondsToBeRemoved)).values.sumOf { it.amount }
if (notRemovedDiamonds == 0) {
val addMap = inventory.addItem(Shard.createItemStack(shardsChange))
if (addMap.isNotEmpty()) {
if (addMap.values.sumOf { it.amount } != 0) {
val excessShards = addMap.values.sumOf { it.amount }.toLong()
balanceManager.addToBankShards(inventory.holder, excessShards).getOrElse {
return Result.failure(it)
@@ -33,7 +37,11 @@ object InventorySnapshotUtils {
val player = Bukkit.getPlayer(inventory.holder)
player?.sendMessage(
mm.deserialize(
"${config.prefix}<reset>: <yellow>The excess change of ${CommonOperations.shardsToDiamondsFull(excessShards)} <yellow> has been deposited into your bank account"
"${config.prefix}<reset>: <yellow>The excess change of ${
CommonOperations.shardsToDiamondsFull(
excessShards
)
} <yellow> has been deposited into your bank account"
)
)
}
@@ -48,24 +56,28 @@ object InventorySnapshotUtils {
if (notRemovedDiamondBlocks == 0) {
val shardsAddMap = inventory.addItem(Shard.createItemStack(shardsChange))
val excessShards = shardsAddMap.values.sumOf { it.amount }.toLong()
if (shardsAddMap.isNotEmpty()) {
if (shardsAddMap.values.sumOf { it.amount } != 0) {
balanceManager.addToBankShards(inventory.holder, excessShards).getOrElse {
return Result.failure(it)
}
}
val diamondsAddMap = inventory.addItem(ItemStack(Material.DIAMOND, diamondsChange))
val excessDiamondsInShards = diamondsAddMap.values.sumOf { it.amount }.toLong() * 9
if (diamondsAddMap.isNotEmpty()) {
if (diamondsAddMap.values.sumOf { it.amount } != 0) {
balanceManager.addToBankShards(inventory.holder, excessDiamondsInShards).getOrElse {
return Result.failure(it)
}
}
if (shardsAddMap.isNotEmpty() || diamondsAddMap.isNotEmpty()) {
if (shardsAddMap.values.sumOf { it.amount } != 0 || diamondsAddMap.values.sumOf { it.amount } != 0) {
val player = Bukkit.getPlayer(inventory.holder)
player?.sendMessage(
mm.deserialize(
"${config.prefix}<reset>: <yellow>The excess change of ${CommonOperations.shardsToDiamondsFull(excessShards+excessDiamondsInShards)} <yellow> has been deposited into your bank account"
"${config.prefix}<reset>: <yellow>The excess change of ${
CommonOperations.shardsToDiamondsFull(
excessShards + excessDiamondsInShards
)
} <yellow> has been deposited into your bank account"
)
)
}
@@ -2,7 +2,7 @@ package net.trueog.diamondbankog
import java.util.*
import kotlinx.coroutines.future.await
import net.trueog.diamondbankog.DiamondBankOG.Companion.luckPerms
import net.luckperms.api.LuckPerms
internal object PlayerPrefix {
private val legacyToMmMap =
@@ -38,7 +38,7 @@ internal object PlayerPrefix {
return legacyRegex.replace(text) { legacyToMmMap[it.groupValues[1].lowercase()] ?: it.value }
}
suspend fun getPrefix(uuid: UUID): String {
suspend fun getPrefix(uuid: UUID, luckPerms: LuckPerms): String {
val user = luckPerms.userManager.getUser(uuid) ?: luckPerms.userManager.loadUser(uuid).await()
val prefixNode = user.nodes.singleOrNull { node -> node.key.startsWith("prefix.1.") }
if (prefixNode == null) return ""
@@ -12,7 +12,7 @@ import org.bukkit.inventory.ShapelessRecipe
import org.bukkit.persistence.PersistentDataType
internal object Shard {
val namespacedKey = NamespacedKey(DiamondBankOG.plugin, "diamondshard")
val namespacedKey = NamespacedKey("diamondbank-og", "diamondshard")
/** amount defaults to 1 */
fun createItemStack(amount: Int = 1): ItemStack {
@@ -27,12 +27,12 @@ internal object Shard {
}
fun createCraftingRecipes() {
val diamondToShardKey = NamespacedKey(DiamondBankOG.plugin, "diamond_to_shard")
val diamondToShardKey = NamespacedKey("diamondbank-og", "diamond_to_shard")
val diamondToShardRecipe = ShapelessRecipe(diamondToShardKey, createItemStack(9))
diamondToShardRecipe.addIngredient(Material.DIAMOND)
Bukkit.addRecipe(diamondToShardRecipe)
val shardToDiamondKey = NamespacedKey(DiamondBankOG.plugin, "shard_to_diamond")
val shardToDiamondKey = NamespacedKey("diamondbank-og", "shard_to_diamond")
val shardToDiamondRecipe = ShapedRecipe(shardToDiamondKey, ItemStack(Material.DIAMOND))
shardToDiamondRecipe.shape("SSS", "SSS", "SSS")
shardToDiamondRecipe.setIngredient('S', createItemStack())
@@ -0,0 +1,88 @@
package net.trueog.diamondbankog
import java.io.File
import kotlin.properties.Delegates
import net.trueog.diamondbankog.DiamondBankOG.Companion.plugin
import org.bukkit.configuration.file.YamlConfiguration
internal class YamlConfig private constructor() : Config {
override lateinit var prefix: String
override lateinit var postgresUrl: String
override lateinit var postgresUser: String
override lateinit var postgresPassword: String
override lateinit var postgresTable: String
override lateinit var postgresLogTable: String
override var postgresLogLimit by Delegates.notNull<Int>()
override lateinit var redisUrl: String
companion object : ConfigFactory {
override fun create(): YamlConfig? {
val config = YamlConfig()
plugin.saveDefaultConfig()
val file = File(plugin.dataFolder, "config.yml")
val yamlConfig = YamlConfiguration.loadConfiguration(file)
try {
config.prefix = yamlConfig.get("prefix") as String
} catch (_: Exception) {
plugin.logger.severe("Failed to parse config option \"prefix\" as a string")
return null
}
try {
config.postgresUrl = yamlConfig.get("postgresUrl") as String
} catch (_: Exception) {
plugin.logger.severe("Failed to parse config option \"postgresUrl\" as a string")
return null
}
try {
config.postgresUser = yamlConfig.get("postgresUser") as String
} catch (_: Exception) {
plugin.logger.severe("Failed to parse config option \"postgresUser\" as a string")
return null
}
try {
config.postgresPassword = yamlConfig.get("postgresPassword") as String
} catch (_: Exception) {
plugin.logger.severe("Failed to parse config option \"postgresPassword\" as a string")
return null
}
try {
config.postgresTable = yamlConfig.get("postgresTable") as String
} catch (_: Exception) {
plugin.logger.severe("Failed to parse config option \"postgresTable\" as a string")
return null
}
try {
config.postgresLogTable = yamlConfig.get("postgresLogTable") as String
} catch (_: Exception) {
plugin.logger.severe("Failed to parse config option \"postgresLogTable\" as a string")
return null
}
try {
config.postgresLogLimit = yamlConfig.get("postgresLogLimit") as Int
if (config.postgresLogLimit < 1) {
plugin.logger.severe("\"postgresLogLimit\" cannot be less than 1")
return null
}
} catch (_: Exception) {
plugin.logger.severe("Failed to parse config option \"postgresLogLimit\" as an integer")
return null
}
try {
config.redisUrl = yamlConfig.get("redisUrl") as String
} catch (_: Exception) {
plugin.logger.severe("Failed to parse config option \"redisUrl\" as a string")
return null
}
return config
}
}
}
@@ -224,7 +224,7 @@ class DiamondBankAPIJava {
val inventorySnapshot = runOnMainThread {
player.inventory.lock()
InventorySnapshot.from(player.inventory)
InventorySnapshot.from(player.inventory, balanceManager)
}
CommonOperations.consume(player.uniqueId, shards, inventorySnapshot).getOrElse {
@@ -281,7 +281,7 @@ class DiamondBankAPIJava {
val inventorySnapshot = runOnMainThread {
payer.inventory.lock()
InventorySnapshot.from(payer.inventory)
InventorySnapshot.from(payer.inventory, balanceManager)
}
CommonOperations.consume(payer.uniqueId, shards, inventorySnapshot).getOrElse {
@@ -1,7 +1,6 @@
package net.trueog.diamondbankog.api
import java.util.*
import kotlin.getOrElse
import kotlinx.coroutines.DelicateCoroutinesApi
import net.trueog.diamondbankog.CommonOperations
import net.trueog.diamondbankog.DiamondBankException.*
@@ -154,7 +153,7 @@ class DiamondBankAPIKotlin {
val inventorySnapshot = runOnMainThread {
player.inventory.lock()
InventorySnapshot.from(player.inventory)
InventorySnapshot.from(player.inventory, balanceManager)
}
CommonOperations.consume(player.uniqueId, shards, inventorySnapshot).getOrElse {
@@ -208,7 +207,7 @@ class DiamondBankAPIKotlin {
val inventorySnapshot = runOnMainThread {
payer.inventory.lock()
InventorySnapshot.from(payer.inventory)
InventorySnapshot.from(payer.inventory, balanceManager)
}
CommonOperations.consume(payer.uniqueId, shards, inventorySnapshot).getOrElse {
@@ -3,6 +3,7 @@ package net.trueog.diamondbankog.commands
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.launch
import net.trueog.diamondbankog.CommonOperations
import net.trueog.diamondbankog.DiamondBankOG
import net.trueog.diamondbankog.DiamondBankOG.Companion.balanceManager
import net.trueog.diamondbankog.DiamondBankOG.Companion.config
import net.trueog.diamondbankog.DiamondBankOG.Companion.economyDisabled
@@ -83,7 +84,8 @@ internal class Balance : CommandExecutor {
"${config.prefix}<reset>: <green>${
if (if (sender is Player) sender.uniqueId == balancePlayer.uniqueId else false) "Your Balance" else "Balance of ${
getPrefix(
balancePlayer.uniqueId
balancePlayer.uniqueId,
DiamondBankOG.luckPerms,
)
}${balancePlayer.name}"
}<reset><green>:\n" +
@@ -4,6 +4,7 @@ import kotlin.math.ceil
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.launch
import net.trueog.diamondbankog.CommonOperations
import net.trueog.diamondbankog.DiamondBankOG
import net.trueog.diamondbankog.DiamondBankOG.Companion.balanceManager
import net.trueog.diamondbankog.DiamondBankOG.Companion.config
import net.trueog.diamondbankog.DiamondBankOG.Companion.economyDisabled
@@ -97,7 +98,8 @@ internal class Balancetop : CommandExecutor {
baltopMessage +=
"\n<green>${baltopMessage.lines().size + offset}<reset>. ${if (playerName == player.name) "<italic>" else ""}${
getPrefix(
uuid
uuid,
DiamondBankOG.luckPerms,
)
}$playerName<reset>: <yellow>$diamonds <aqua>Diamonds"
}
@@ -157,7 +159,8 @@ internal class Balancetop : CommandExecutor {
baltopMessage +=
"\n<green>${baltopMessage.lines().size + (9 * (index - 1))}<reset>. ${if (playerName == player.name) "<italic>" else ""}${
getPrefix(
uuid
uuid,
DiamondBankOG.luckPerms,
)
}$playerName<reset>: <yellow>$diamonds <aqua>Diamonds"
}
@@ -2,16 +2,16 @@ package net.trueog.diamondbankog.commands
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import net.trueog.diamondbankog.DiamondBankOG.Companion.config
import net.kyori.adventure.text.minimessage.MiniMessage
import net.trueog.diamondbankog.Config
import net.trueog.diamondbankog.DiamondBankOG.Companion.economyDisabled
import net.trueog.diamondbankog.DiamondBankOG.Companion.mm
import org.bukkit.command.CommandSender
import org.bukkit.entity.Player
object CommonCommandInterlude {
/** @return should return */
@OptIn(ExperimentalContracts::class)
fun run(sender: CommandSender, command: String): Boolean {
fun run(sender: CommandSender, command: String, config: Config, mm: MiniMessage): Boolean {
contract { returns(false) implies (sender is Player) }
if (economyDisabled) {
sender.sendMessage(
@@ -1,20 +1,16 @@
package net.trueog.diamondbankog.commands
import kotlin.math.abs
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import net.trueog.diamondbankog.DiamondBankOG.Companion.config
import net.trueog.diamondbankog.DiamondBankOG.Companion.mm
import net.trueog.diamondbankog.DiamondBankOG.Companion.scope
import net.trueog.diamondbankog.DiamondBankOG.Companion.transactionLock
import net.kyori.adventure.text.minimessage.MiniMessage
import net.trueog.diamondbankog.*
import net.trueog.diamondbankog.InventoryExtensions.countDiamondBlocks
import net.trueog.diamondbankog.InventoryExtensions.countDiamonds
import net.trueog.diamondbankog.InventoryExtensions.countShards
import net.trueog.diamondbankog.InventoryExtensions.lock
import net.trueog.diamondbankog.InventoryExtensions.unlock
import net.trueog.diamondbankog.InventorySnapshot
import net.trueog.diamondbankog.MainThreadBlock.runOnMainThread
import net.trueog.diamondbankog.Shard
import net.trueog.diamondbankog.TransactionLock
import org.bukkit.Material
import org.bukkit.block.ShulkerBox
import org.bukkit.command.Command
@@ -23,9 +19,15 @@ import org.bukkit.command.CommandSender
import org.bukkit.inventory.ItemStack
import org.bukkit.inventory.meta.BlockStateMeta
internal class Compress : CommandExecutor {
internal class Compress(
val config: Config = DiamondBankOG.config,
val balanceManager: BalanceManager = DiamondBankOG.balanceManager,
val mm: MiniMessage = DiamondBankOG.mm,
val scope: CoroutineScope = DiamondBankOG.scope,
val transactionLock: TransactionLock = DiamondBankOG.transactionLock,
) : CommandExecutor {
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>?): Boolean {
if (CommonCommandInterlude.run(sender, "compress")) {
if (CommonCommandInterlude.run(sender, "compress", config, mm)) {
return true
}
@@ -38,7 +40,7 @@ internal class Compress : CommandExecutor {
transactionLock.tryWithLockSuspend(sender.uniqueId) {
val inventorySnapshot = runOnMainThread {
sender.inventory.lock()
InventorySnapshot.from(sender.inventory)
InventorySnapshot.from(sender.inventory, balanceManager)
}
val (inventory, blockStateMeta, blockState) =
@@ -147,6 +149,8 @@ internal class Compress : CommandExecutor {
}
}
val summaryStringBuilder = StringBuilder("Compression Summary:")
if (changeInShards < 0) {
val removeMap = inventory.removeItem(Shard.createItemStack(abs(changeInShards)))
if (removeMap.isNotEmpty()) {
@@ -158,6 +162,9 @@ internal class Compress : CommandExecutor {
sender.inventory.unlock()
return@tryWithLockSuspend
}
summaryStringBuilder.append(
"\n<red>$changeInShards Diamond Shard${if (changeInShards != -1) "s" else ""}"
)
}
if (changeInDiamonds > 0) {
@@ -171,6 +178,9 @@ internal class Compress : CommandExecutor {
sender.inventory.unlock()
return@tryWithLockSuspend
}
summaryStringBuilder.append(
"\n<green>+$changeInDiamonds Diamond${if (changeInDiamonds != 1) "s" else ""}"
)
} else if (changeInDiamonds < 0) {
val removeMap = inventory.removeItem(ItemStack(Material.DIAMOND, abs(changeInDiamonds)))
if (removeMap.isNotEmpty()) {
@@ -182,6 +192,9 @@ internal class Compress : CommandExecutor {
sender.inventory.unlock()
return@tryWithLockSuspend
}
summaryStringBuilder.append(
"\n<red>$changeInDiamonds Diamond${if (changeInDiamonds != -1) "s" else ""}"
)
}
if (changeInDiamondBlocks > 0) {
@@ -195,18 +208,9 @@ internal class Compress : CommandExecutor {
sender.inventory.unlock()
return@tryWithLockSuspend
}
} else if (changeInDiamondBlocks < 0) {
val removeMap =
inventory.removeItem(ItemStack(Material.DIAMOND_BLOCK, abs(changeInDiamondBlocks)))
if (removeMap.isNotEmpty()) {
sender.sendMessage(
mm.deserialize(
"${config.prefix}<reset>: <red>Something went wrong while trying to compress the Diamond currency items in your ${if (isShulkerBox) "shulker box" else "inventory"}."
)
)
sender.inventory.unlock()
return@tryWithLockSuspend
}
summaryStringBuilder.append(
"\n<green>+$changeInDiamondBlocks Diamond Block${if (changeInDiamondBlocks != 1) "s" else ""}"
)
}
if (blockStateMeta != null && blockState != null) {
@@ -233,44 +237,7 @@ internal class Compress : CommandExecutor {
)
)
val shardsLine =
if (changeInShards < 0)
"|<red>$changeInShards Diamond Shard${if (abs(changeInShards) != 1) "s" else ""}\n"
else ""
val diamondsLine =
(if (changeInDiamonds > 0) "|<green>+$changeInDiamonds"
else if (changeInDiamonds < 0) "|<red>$changeInDiamonds" else "") +
if (changeInDiamonds != 0)
" Diamond${
if (abs(
changeInDiamonds
) != 1
) "s" else ""
}\n"
else ""
val diamondBlocksLine =
(if (changeInDiamondBlocks > 0) "|<green>+$changeInDiamondBlocks"
else if (changeInDiamondBlocks < 0) "|<red>$changeInDiamondBlocks" else "") +
if (changeInDiamondBlocks != 0)
" Diamond Block${
if (abs(
changeInDiamondBlocks
) != 1
) "s" else ""
}"
else ""
sender.sendMessage(
mm.deserialize(
buildString {
append("${config.prefix}<reset>: Compression Summary:\n")
if (shardsLine.isNotEmpty()) append(shardsLine)
if (diamondsLine.isNotEmpty()) append(diamondsLine)
if (diamondBlocksLine.isNotEmpty()) append(diamondBlocksLine)
}
.trimMargin("|")
)
)
sender.sendMessage(mm.deserialize("${config.prefix}<reset>: $summaryStringBuilder"))
}
) {
is TransactionLock.LockResult.Failed -> {
@@ -1,28 +1,28 @@
package net.trueog.diamondbankog.commands
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.launch
import net.trueog.diamondbankog.CommonOperations
import net.trueog.diamondbankog.DiamondBankOG.Companion.balanceManager
import net.trueog.diamondbankog.DiamondBankOG.Companion.config
import net.trueog.diamondbankog.DiamondBankOG.Companion.mm
import net.trueog.diamondbankog.DiamondBankOG.Companion.scope
import net.trueog.diamondbankog.DiamondBankOG.Companion.transactionLock
import net.kyori.adventure.text.minimessage.MiniMessage
import net.trueog.diamondbankog.*
import net.trueog.diamondbankog.ErrorHandler.handleError
import net.trueog.diamondbankog.InventoryExtensions.lock
import net.trueog.diamondbankog.InventoryExtensions.unlock
import net.trueog.diamondbankog.InventorySnapshot
import net.trueog.diamondbankog.InventorySnapshotUtils
import net.trueog.diamondbankog.MainThreadBlock.runOnMainThread
import net.trueog.diamondbankog.TransactionLock
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
internal class Deposit : CommandExecutor {
internal class Deposit(
val config: Config = DiamondBankOG.config,
val balanceManager: BalanceManager = DiamondBankOG.balanceManager,
val mm: MiniMessage = DiamondBankOG.mm,
val scope: CoroutineScope = DiamondBankOG.scope,
val transactionLock: TransactionLock = DiamondBankOG.transactionLock,
) : CommandExecutor {
@OptIn(DelicateCoroutinesApi::class)
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>?): Boolean {
if (CommonCommandInterlude.run(sender, "deposit")) {
if (CommonCommandInterlude.run(sender, "deposit", config, mm)) {
return true
}
@@ -74,21 +74,20 @@ internal class Deposit : CommandExecutor {
transactionLock.tryWithLockSuspend(sender.uniqueId) {
val inventorySnapshot = runOnMainThread {
sender.inventory.lock()
InventorySnapshot.from(sender.inventory)
InventorySnapshot.from(sender.inventory, balanceManager)
}
val removedInShards: Long =
if (shards == -1L) {
InventorySnapshotUtils.removeAll(inventorySnapshot).toLong()
} else {
InventorySnapshotUtils.removeShards(inventorySnapshot, shards)
InventorySnapshotUtils.removeShards(inventorySnapshot, shards, config, balanceManager, mm)
.getOrElse {
sender.sendMessage(
mm.deserialize(
"${config.prefix}<reset>: <red>You do not have enough inventory space for the change."
)
mm.deserialize("${config.prefix}<reset>: <red>Something went wrong.")
)
sender.inventory.unlock()
handleError(it)
return@tryWithLockSuspend
}
.toLong()
@@ -96,7 +95,11 @@ internal class Deposit : CommandExecutor {
if (shards != -1L && removedInShards != shards) {
sender.sendMessage(
mm.deserialize(
"${config.prefix}<reset>: <red>You do not have ${CommonOperations.shardsToDiamondsFull(shards)} <red>to deposit."
"${config.prefix}<reset>: <red>You do not have ${
CommonOperations.shardsToDiamondsFull(
shards
)
} <red>to deposit."
)
)
sender.inventory.unlock()
@@ -120,7 +123,11 @@ internal class Deposit : CommandExecutor {
sender.sendMessage(
mm.deserialize(
"${config.prefix}<reset>: <green>Successfully deposited ${CommonOperations.shardsToDiamondsFull(removedInShards)} <green>into your bank account."
"${config.prefix}<reset>: <green>Successfully deposited ${
CommonOperations.shardsToDiamondsFull(
removedInShards
)
} <green>into your bank account."
)
)
@@ -1,11 +1,11 @@
package net.trueog.diamondbankog.commands
import net.trueog.diamondbankog.BalanceManager
import net.trueog.diamondbankog.Config
import net.trueog.diamondbankog.CachingBalanceManager
import net.trueog.diamondbankog.DiamondBankOG.Companion.balanceManager
import net.trueog.diamondbankog.DiamondBankOG.Companion.config
import net.trueog.diamondbankog.DiamondBankOG.Companion.mm
import net.trueog.diamondbankog.DiamondBankOG.Companion.plugin
import net.trueog.diamondbankog.YamlConfig
import org.bukkit.Bukkit
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
@@ -14,7 +14,7 @@ import org.bukkit.command.CommandSender
internal class DiamondBankReload : CommandExecutor {
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>?): Boolean {
config =
Config.create()
YamlConfig.create()
?: run {
sender.sendMessage(
mm.deserialize("<red>Failed to reload the config. Check the console for more information.")
@@ -23,7 +23,7 @@ internal class DiamondBankReload : CommandExecutor {
return true
}
balanceManager = BalanceManager()
balanceManager = CachingBalanceManager()
try {
balanceManager.init()
} catch (e: Exception) {
@@ -1,29 +1,31 @@
package net.trueog.diamondbankog.commands
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.launch
import net.trueog.diamondbankog.CommonOperations
import net.trueog.diamondbankog.DiamondBankException
import net.trueog.diamondbankog.DiamondBankOG.Companion.balanceManager
import net.trueog.diamondbankog.DiamondBankOG.Companion.config
import net.trueog.diamondbankog.DiamondBankOG.Companion.mm
import net.trueog.diamondbankog.DiamondBankOG.Companion.scope
import net.trueog.diamondbankog.DiamondBankOG.Companion.transactionLock
import net.kyori.adventure.text.minimessage.MiniMessage
import net.luckperms.api.LuckPerms
import net.trueog.diamondbankog.*
import net.trueog.diamondbankog.ErrorHandler.handleError
import net.trueog.diamondbankog.InventoryExtensions.lock
import net.trueog.diamondbankog.InventoryExtensions.unlock
import net.trueog.diamondbankog.InventorySnapshot
import net.trueog.diamondbankog.MainThreadBlock.runOnMainThread
import net.trueog.diamondbankog.PlayerPrefix.getPrefix
import net.trueog.diamondbankog.TransactionLock
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
internal class Pay : CommandExecutor {
internal class Pay(
val config: Config = DiamondBankOG.config,
val balanceManager: BalanceManager = DiamondBankOG.balanceManager,
val mm: MiniMessage = DiamondBankOG.mm,
val scope: CoroutineScope = DiamondBankOG.scope,
val luckPerms: LuckPerms = DiamondBankOG.luckPerms,
val transactionLock: TransactionLock = DiamondBankOG.transactionLock,
) : CommandExecutor {
@OptIn(DelicateCoroutinesApi::class)
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>?): Boolean {
if (CommonCommandInterlude.run(sender, "pay")) {
if (CommonCommandInterlude.run(sender, "pay", config, mm)) {
return true
}
@@ -87,32 +89,37 @@ internal class Pay : CommandExecutor {
transactionLock.tryWithLockSuspend(sender.uniqueId) {
val inventorySnapshot = runOnMainThread {
sender.inventory.lock()
InventorySnapshot.from(sender.inventory)
InventorySnapshot.from(sender.inventory, balanceManager)
}
CommonOperations.consume(sender.uniqueId, shards, inventorySnapshot).getOrElse {
when (it) {
is DiamondBankException.InsufficientFundsException -> {
sender.sendMessage(
mm.deserialize(
"${config.prefix}<reset>: <red>You are ${CommonOperations.shardsToDiamondsFull(it.short)} <red>short for that payment."
CommonOperations.consume(sender.uniqueId, shards, inventorySnapshot, config, balanceManager, mm)
.getOrElse {
when (it) {
is DiamondBankException.InsufficientFundsException -> {
sender.sendMessage(
mm.deserialize(
"${config.prefix}<reset>: <red>You are ${
CommonOperations.shardsToDiamondsFull(
it.short
)
} <red>short for that payment."
)
)
)
sender.inventory.unlock()
return@tryWithLockSuspend
}
sender.inventory.unlock()
return@tryWithLockSuspend
}
else -> {
sender.sendMessage(
mm.deserialize(
"${config.prefix}<reset>: <red>A severe error has occurred. Please notify a staff member."
else -> {
sender.sendMessage(
mm.deserialize(
"${config.prefix}<reset>: <red>A severe error has occurred. Please notify a staff member."
)
)
)
sender.inventory.unlock()
return@tryWithLockSuspend
sender.inventory.unlock()
return@tryWithLockSuspend
}
}
}
}
balanceManager.addToBankShards(receiver.uniqueId, shards).getOrElse {
handleError(it)
@@ -132,9 +139,14 @@ internal class Pay : CommandExecutor {
sender.sendMessage(
mm.deserialize(
"${config.prefix}<reset>: <green>Successfully paid ${CommonOperations.shardsToDiamondsFull(shards)} <green>to ${
"${config.prefix}<reset>: <green>Successfully paid ${
CommonOperations.shardsToDiamondsFull(
shards
)
} <green>to ${
getPrefix(
receiver.uniqueId
receiver.uniqueId,
luckPerms,
)
} ${receiver.name}<reset><green>."
)
@@ -146,9 +158,14 @@ internal class Pay : CommandExecutor {
mm.deserialize(
"${config.prefix}<reset>: <green>${
getPrefix(
sender.uniqueId
sender.uniqueId,
luckPerms,
)
} ${sender.name}<reset> <green>has paid you ${CommonOperations.shardsToDiamondsFull(shards)}<green>."
} ${sender.name}<reset> <green>has paid you ${
CommonOperations.shardsToDiamondsFull(
shards
)
}<green>."
)
)
}
@@ -3,6 +3,7 @@ package net.trueog.diamondbankog.commands
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.launch
import net.trueog.diamondbankog.CommonOperations
import net.trueog.diamondbankog.DiamondBankOG
import net.trueog.diamondbankog.DiamondBankOG.Companion.balanceManager
import net.trueog.diamondbankog.DiamondBankOG.Companion.config
import net.trueog.diamondbankog.DiamondBankOG.Companion.economyDisabled
@@ -80,7 +81,8 @@ internal class SetBankBalance : CommandExecutor {
mm.deserialize(
"${config.prefix}<reset>: <green>Successfully set the balance of ${
getPrefix(
player.uniqueId
player.uniqueId,
DiamondBankOG.luckPerms,
)
}${player.name}<reset> <green>to <yellow>$balance <aqua>${if (balance == 1L) "Shard" else "Shards"}<green>."
)
@@ -1,31 +1,31 @@
package net.trueog.diamondbankog.commands
import kotlin.math.floor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.launch
import net.trueog.diamondbankog.CommonOperations
import net.trueog.diamondbankog.DiamondBankOG.Companion.balanceManager
import net.trueog.diamondbankog.DiamondBankOG.Companion.config
import net.trueog.diamondbankog.DiamondBankOG.Companion.mm
import net.trueog.diamondbankog.DiamondBankOG.Companion.scope
import net.trueog.diamondbankog.DiamondBankOG.Companion.transactionLock
import net.kyori.adventure.text.minimessage.MiniMessage
import net.trueog.diamondbankog.*
import net.trueog.diamondbankog.ErrorHandler.handleError
import net.trueog.diamondbankog.InventoryExtensions.lock
import net.trueog.diamondbankog.InventoryExtensions.unlock
import net.trueog.diamondbankog.InventorySnapshot
import net.trueog.diamondbankog.MainThreadBlock.runOnMainThread
import net.trueog.diamondbankog.Shard
import net.trueog.diamondbankog.TransactionLock
import org.bukkit.Material
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
import org.bukkit.inventory.ItemStack
internal class Withdraw : CommandExecutor {
internal class Withdraw(
val config: Config = DiamondBankOG.config,
val balanceManager: BalanceManager = DiamondBankOG.balanceManager,
val mm: MiniMessage = DiamondBankOG.mm,
val scope: CoroutineScope = DiamondBankOG.scope,
val transactionLock: TransactionLock = DiamondBankOG.transactionLock,
) : CommandExecutor {
@OptIn(DelicateCoroutinesApi::class)
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>?): Boolean {
if (CommonCommandInterlude.run(sender, "withdraw")) {
if (CommonCommandInterlude.run(sender, "withdraw", config, mm)) {
return true
}
@@ -37,10 +37,10 @@ internal class Withdraw : CommandExecutor {
)
return true
}
if (args.size != 1 && args.size != 2) {
if (args.size != 1) {
sender.sendMessage(
mm.deserialize(
"${config.prefix}<reset>: <red>Please provide the amount of <aqua>Diamonds <red>you want to withdraw. Either a number or \"all\"."
"${config.prefix}<reset>: <red>Please (only) provide the amount of <aqua>Diamonds <red>you want to withdraw. Either a number or \"all\"."
)
)
return true
@@ -72,12 +72,14 @@ internal class Withdraw : CommandExecutor {
}
}
sender.inventory.lock()
val inventorySnapshot = InventorySnapshot.from(sender.inventory)
scope.launch {
when (
transactionLock.tryWithLockSuspend(sender.uniqueId) {
val inventorySnapshot = runOnMainThread {
sender.inventory.lock()
InventorySnapshot.from(sender.inventory, balanceManager)
}
val bankShards =
balanceManager.getBankShards(sender.uniqueId).getOrElse {
sender.inventory.unlock()
@@ -100,7 +102,15 @@ internal class Withdraw : CommandExecutor {
sender.inventory.unlock()
sender.sendMessage(
mm.deserialize(
"${config.prefix}<reset>: <red>Cannot withdraw ${CommonOperations.shardsToDiamondsFull(shardsToWithdraw)} <red>because your bank only contains <yellow>${CommonOperations.shardsToDiamondsFull(bankShards)}<red>."
"${config.prefix}<reset>: <red>Cannot withdraw ${
CommonOperations.shardsToDiamondsFull(
shardsToWithdraw
)
} <red>because your bank only contains <yellow>${
CommonOperations.shardsToDiamondsFull(
bankShards
)
}<red>."
)
)
return@tryWithLockSuspend
@@ -116,7 +126,11 @@ internal class Withdraw : CommandExecutor {
sender.inventory.unlock()
sender.sendMessage(
mm.deserialize(
"${config.prefix}<reset>: <red>You don't have enough inventory space to withdraw ${CommonOperations.shardsToDiamondsFull(shardsToWithdraw)}<red>."
"${config.prefix}<reset>: <red>You don't have enough inventory space to withdraw ${
CommonOperations.shardsToDiamondsFull(
shardsToWithdraw
)
}<red>."
)
)
return@tryWithLockSuspend
@@ -140,7 +154,11 @@ internal class Withdraw : CommandExecutor {
sender.sendMessage(
mm.deserialize(
"${config.prefix}<reset>: <green>Successfully withdrew ${CommonOperations.shardsToDiamondsFull(shardsToWithdraw)} <green>from your bank account."
"${config.prefix}<reset>: <green>Successfully withdrew ${
CommonOperations.shardsToDiamondsFull(
shardsToWithdraw
)
} <green>from your bank account."
)
)
@@ -0,0 +1,317 @@
package net.trueog.diamondbankog
import io.mockk.*
import net.kyori.adventure.text.Component
import net.trueog.diamondbankog.Utils.allImpl
import org.bukkit.Bukkit
import org.bukkit.Material
import org.bukkit.NamespacedKey
import org.bukkit.Server
import org.bukkit.block.ShulkerBox
import org.bukkit.inventory.Inventory
import org.bukkit.inventory.InventoryHolder
import org.bukkit.inventory.ItemStack
import org.bukkit.inventory.meta.BlockStateMeta
import org.bukkit.inventory.meta.Damageable
import org.bukkit.inventory.meta.ItemMeta
import org.bukkit.persistence.PersistentDataType
object BukkitMock {
private fun first(item: ItemStack?, inventory: Array<ItemStack?>): Int {
if (item == null) {
return -1
}
for (i in inventory.indices) {
if (inventory[i] == null) continue
if (item.isSimilar(inventory[i])) {
return i
}
}
return -1
}
private fun firstPartial(item: ItemStack?, inventory: Array<ItemStack?>): Int {
val filteredItem = item?.clone()
if (item == null) {
return -1
}
for (i in inventory.indices) {
val cItem = inventory[i]
if (cItem != null && cItem.amount < 64 && cItem.isSimilar(filteredItem)) {
return i
}
}
return -1
}
private fun firstEmpty(inventory: Array<ItemStack?>): Int {
for (i in inventory.indices) {
if (inventory[i] == null) {
return i
}
}
return -1
}
const val MAX_AMOUNT = 64
fun mockBukkit(): Server {
val server = mockk<Server>()
mockkStatic(Bukkit::class)
every { Bukkit.getServer() } returns server
every { server.isPrimaryThread } returns true
every { Bukkit.isPrimaryThread() } returns true
every { Bukkit.getItemFactory() } answers { server.itemFactory }
every { Bukkit.createInventory(anyNullable<InventoryHolder>(), any<Int>()) } answers
{
server.createInventory(firstArg<InventoryHolder?>(), secondArg<Int>())
}
val sizeSlot = slot<Int>()
every { server.createInventory(any(), capture(sizeSlot)) } returns createInventory()
every { server.itemFactory.getItemMeta(any()) } answers
{
val material = firstArg<Material>()
val itemMeta =
if (material == Material.SHULKER_BOX) {
mockk<BlockStateMeta>()
} else {
mockk<Damageable>()
}
val persistentDataContainerKeys = mutableSetOf<NamespacedKey>()
var displayName: Component? = null
every { itemMeta.clone() } returns itemMeta
every { itemMeta.displayName(any()) } answers { displayName = firstArg<Component>() }
every { itemMeta.displayName() } answers { displayName }
every { itemMeta.addEnchant(any(), any(), any()) } returns true
every { itemMeta.addItemFlags(any()) } just Runs
every { itemMeta.persistentDataContainer.set(any(), PersistentDataType.STRING, any()) } answers
{
val key = firstArg<NamespacedKey>()
persistentDataContainerKeys.add(key)
}
every { itemMeta.persistentDataContainer.has(any()) } answers
{
val key = firstArg<NamespacedKey>()
persistentDataContainerKeys.contains(key)
}
every { itemMeta.persistentDataContainer.keys } answers { persistentDataContainerKeys }
every { itemMeta.persistentDataContainer.isEmpty } answers { persistentDataContainerKeys.isEmpty() }
if (itemMeta is BlockStateMeta) {
val blockState = mockk<ShulkerBox>()
var inventory = createInventory()
every { itemMeta.blockState } returns blockState
every { itemMeta.blockState = any() } answers { inventory = firstArg<ShulkerBox>().inventory }
every { blockState.inventory } returns inventory
}
if (itemMeta is Damageable) {
every { itemMeta.damage } returns 0
}
itemMeta
}
every { server.itemFactory.isApplicable(any(), Material.PRISMARINE_SHARD) } returns true
every { server.itemFactory.isApplicable(any(), Material.SHULKER_BOX) } returns true
every { server.itemFactory.asMetaFor(any(), any<Material>()) } answers
{
val itemMeta = firstArg<ItemMeta>()
val persistentDataContainerKeys = mutableSetOf<NamespacedKey>()
val displayName = itemMeta.displayName()
itemMeta.persistentDataContainer.keys.forEach { persistentDataContainerKeys.add(it) }
val material = secondArg<Material>()
val newItemMeta =
if (material == Material.SHULKER_BOX) {
mockk<BlockStateMeta>()
} else {
mockk<Damageable>()
}
every { newItemMeta.clone() } returns newItemMeta
every { newItemMeta.displayName() } answers { displayName }
every { newItemMeta.persistentDataContainer.has(any()) } answers
{
val key = firstArg<NamespacedKey>()
persistentDataContainerKeys.contains(key)
}
every { newItemMeta.persistentDataContainer.isEmpty } answers { persistentDataContainerKeys.isEmpty() }
every { newItemMeta.persistentDataContainer.keys } answers { persistentDataContainerKeys }
if (newItemMeta is BlockStateMeta) {
val blockState = mockk<ShulkerBox>()
var inventory = ((itemMeta as BlockStateMeta).blockState as ShulkerBox).inventory
println(inventory.contents[0])
every { newItemMeta.blockState } returns blockState
every { newItemMeta.blockState = any() } answers { inventory = firstArg<ShulkerBox>().inventory }
every { blockState.inventory } returns inventory
}
if (newItemMeta is Damageable) {
every { newItemMeta.damage } returns 0
}
newItemMeta
}
every { server.itemFactory.equals(any(), any()) } answers
{
val meta1 = firstArg<ItemMeta?>()
val meta2 = secondArg<ItemMeta?>()
if (meta1 == meta2) return@answers true
if (meta1 == null) {
return@answers (meta2!!.displayName() == null &&
!meta2.hasEnchants() &&
meta2.persistentDataContainer.isEmpty)
}
if (meta2 == null) {
return@answers (meta1.displayName() == null &&
!meta1.hasEnchants() &&
meta1.persistentDataContainer.isEmpty)
}
meta1.displayName() == meta2.displayName() &&
meta1.persistentDataContainer.keys == meta2.persistentDataContainer.keys
}
every { @Suppress("DEPRECATION") server.itemFactory.updateMaterial(any(), Material.PRISMARINE_SHARD) } returns
Material.PRISMARINE_SHARD
every { @Suppress("DEPRECATION") server.itemFactory.updateMaterial(any(), Material.SHULKER_BOX) } returns
Material.SHULKER_BOX
return server
}
fun createInventory(): Inventory {
val inventory = mockk<Inventory>()
var contents = arrayOfNulls<ItemStack>(36)
every { inventory.contents = any<Array<ItemStack?>>() } answers { contents = firstArg<Array<ItemStack?>>() }
every { inventory.contents } answers { contents }
every { inventory.storageContents } answers { contents }
every { inventory.all(any<Material>()) } answers
{
val material = firstArg<Material>()
allImpl(contents, material)
}
every { inventory.addItem(any()) } answers { addToInventory(firstArg<Array<out ItemStack?>>(), contents) }
every { inventory.removeItem(any()) } answers
{
val items = firstArg<Array<out ItemStack?>>()
val leftover = HashMap<Int?, ItemStack?>()
items.forEachIndexed { index, item ->
if (item == null) {
throw IllegalArgumentException("ItemStack cannot be null")
}
if (item.type.isAir) {
return@forEachIndexed
}
var toDelete = item.amount
while (true) {
val first = first(item, contents)
if (first == -1) {
item.amount = toDelete
leftover[index] = item
break
} else {
val itemStack = contents[first]
val amount = itemStack!!.amount
if (amount <= toDelete) {
toDelete -= amount
contents[first] = null
} else {
itemStack.amount = amount - toDelete
contents[first] = itemStack
toDelete = 0
}
}
if (toDelete <= 0) {
break
}
}
}
leftover
}
every { inventory.remove(anyNullable<Material>()) } answers
{
val material = firstArg<Material?>() ?: throw IllegalArgumentException("Material cannot be null")
contents.forEachIndexed { index, item ->
if (item?.type == material) {
contents[index] = null
}
}
}
every { inventory.getItem(any<Int>()) } answers { contents[firstArg()] }
return inventory
}
fun addToInventory(items: Array<out ItemStack?>, contents: Array<ItemStack?>): HashMap<Int?, ItemStack?> {
val leftover = HashMap<Int?, ItemStack?>()
items.forEachIndexed { index, item ->
if (item == null) {
throw IllegalArgumentException("ItemStack cannot be null")
}
if (item.type.isAir) {
return@forEachIndexed
}
while (true) {
val firstPartial = firstPartial(item, contents)
if (firstPartial == -1) {
val firstFree = firstEmpty(contents)
if (firstFree == -1) {
leftover[index] = item
break
} else {
if (item.amount > MAX_AMOUNT) {
val stack = item.clone()
stack.amount = MAX_AMOUNT
contents[firstFree] = stack.clone()
item.amount = -MAX_AMOUNT
} else {
contents[firstFree] = item.clone()
break
}
}
} else {
val partialItem = contents[firstPartial]?.clone()
val amount = item.amount
val partialAmount = partialItem!!.amount
if (amount + partialAmount <= MAX_AMOUNT) {
partialItem.amount = amount + partialAmount
contents[firstPartial] = partialItem.clone()
break
}
partialItem.amount = MAX_AMOUNT
contents[firstPartial] = partialItem.clone()
item.amount = amount + partialAmount - MAX_AMOUNT
}
}
}
return leftover
}
}
@@ -0,0 +1,393 @@
package net.trueog.diamondbankog
import io.mockk.MockKAnnotations
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.just
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.test.runTest
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.minimessage.MiniMessage
import net.trueog.diamondbankog.Constants.playerUuid
import net.trueog.diamondbankog.InventoryExtensions.countDiamondBlocks
import net.trueog.diamondbankog.InventoryExtensions.countDiamonds
import net.trueog.diamondbankog.InventoryExtensions.countShards
import net.trueog.diamondbankog.InventoryExtensions.isLocked
import net.trueog.diamondbankog.Utils.mockPlayerInventory
import net.trueog.diamondbankog.Utils.waitForCoroutines
import net.trueog.diamondbankog.commands.Compress
import org.bukkit.Bukkit
import org.bukkit.Material
import org.bukkit.Server
import org.bukkit.block.ShulkerBox
import org.bukkit.command.Command
import org.bukkit.entity.Player
import org.bukkit.inventory.ItemStack
import org.bukkit.inventory.meta.BlockStateMeta
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertAll
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
class CompressTest {
@MockK lateinit var config: Config
@MockK lateinit var balanceManager: BalanceManager
@MockK lateinit var mm: MiniMessage
@MockK lateinit var player: Player
@MockK lateinit var command: Command
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
@BeforeEach
fun setup() {
MockKAnnotations.init(this)
every { config.prefix } returns "DiamondBank-OG"
val inputSlot = slot<String>()
every { mm.deserialize(capture(inputSlot)) } answers { Component.text(inputSlot.captured) }
every { player.uniqueId } returns playerUuid
every { player.world.name } returns "world"
every { player.hasPermission("diamondbank-og.compress") } returns true
every { Bukkit.getPlayer("player") } returns player
every { server.getPlayer("player") } returns player
every { Bukkit.getPlayer(playerUuid) } returns player
every { server.getPlayer(playerUuid) } returns player
every { player.sendMessage(any<Component>()) } just Runs
coEvery { balanceManager.setPlayerShards(any(), any(), PostgreSQL.ShardType.INVENTORY) } returns
Result.success(Unit)
}
companion object {
lateinit var server: Server
@JvmStatic
@BeforeAll
fun setupBukkit() {
server = BukkitMock.mockBukkit()
}
@JvmStatic
fun compressData() =
listOf(
Arguments.of("Nothing", arrayOf<ItemStack>(), "<#FFA500>Nothing found to compress.", 0, 0, 0),
Arguments.of(
"9 shards to 1 diamond",
arrayOf(Shard.createItemStack(9)),
"Compression Summary:\n<red>-9 Diamond Shards\n<green>+1 Diamond",
0,
1,
0,
),
Arguments.of(
"9 diamonds to 1 diamond block",
arrayOf(ItemStack(Material.DIAMOND, 9)),
"Compression Summary:\n<red>-9 Diamonds\n<green>+1 Diamond Block",
0,
0,
1,
),
Arguments.of(
"9 diamonds and 9 shards to 1 diamond block and 1 diamond",
arrayOf(Shard.createItemStack(9), ItemStack(Material.DIAMOND, 9)),
"Compression Summary:\n<red>-9 Diamond Shards\n<red>-8 Diamonds\n<green>+1 Diamond Block",
0,
1,
1,
),
)
@JvmStatic
fun overflowCompressData() =
listOf(
Arguments.of(
"10 shards",
Array(35) { ItemStack(Material.DIRT, 1) } + arrayOf(Shard.createItemStack(10)),
"You do not have enough space in your inventory to compress all the Diamond currency items (<green>+1 <aqua>Diamonds<red>).",
10,
0,
0,
),
Arguments.of(
"10 diamonds",
Array(35) { ItemStack(Material.DIRT, 1) } + arrayOf(ItemStack(Material.DIAMOND, 10)),
"You do not have enough space in your inventory to compress all the Diamond currency items (<green>+1 <aqua>Diamond Blocks<red>).",
0,
10,
0,
),
)
}
@ParameterizedTest(name = "{0}")
@DisplayName("Successful Compress")
@MethodSource("compressData")
fun successfulCompress(
@Suppress("UNUSED_PARAMETER") name: String,
inventoryContents: Array<ItemStack?>,
compressionSummary: String,
invShardCount: Long,
invDiamondCount: Long,
invDiamondBlockCount: Long,
) = runTest {
val inventory = mockPlayerInventory(player, playerUuid, inventoryContents)
val compress = Compress(config, balanceManager, mm, scope)
compress.onCommand(player, command, "compress", arrayOf())
waitForCoroutines(scope)
assertAll(
buildList {
add { verify { player.sendMessage(Component.text("DiamondBank-OG<reset>: $compressionSummary")) } }
add { assertEquals(invShardCount, inventory.countShards(), "Shard count") }
add { assertEquals(invDiamondCount, inventory.countDiamonds(), "Diamond count") }
add { assertEquals(invDiamondBlockCount, inventory.countDiamondBlocks(), "Diamond block count") }
add { assertFalse(inventory.isLocked(), "Inventory locked") }
}
)
}
@ParameterizedTest(name = "{0}")
@DisplayName("Shulker box Successful Compress")
@MethodSource("compressData")
fun successfulShulkerBoxCompress(
@Suppress("UNUSED_PARAMETER") name: String,
inventoryContents: Array<ItemStack>,
compressionSummary: String,
invShardCount: Long,
invDiamondCount: Long,
invDiamondBlockCount: Long,
) = runTest {
val shulkerBox = ItemStack(Material.SHULKER_BOX)
val blockStateMeta = shulkerBox.itemMeta as BlockStateMeta
val shulkerBoxState = blockStateMeta.blockState as ShulkerBox
inventoryContents.forEach { shulkerBoxState.inventory.addItem(it) }
blockStateMeta.blockState = shulkerBoxState
shulkerBox.itemMeta = blockStateMeta
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(shulkerBox))
val compress = Compress(config, balanceManager, mm, scope)
compress.onCommand(player, command, "compress", arrayOf("yes"))
waitForCoroutines(scope)
assertAll(
buildList {
add { verify { player.sendMessage(Component.text("DiamondBank-OG<reset>: $compressionSummary")) } }
add { assertEquals(invShardCount, shulkerBoxState.inventory.countShards(), "Shard count") }
add { assertEquals(invDiamondCount, shulkerBoxState.inventory.countDiamonds(), "Diamond count") }
add {
assertEquals(
invDiamondBlockCount,
shulkerBoxState.inventory.countDiamondBlocks(),
"Diamond block count",
)
}
add { assertFalse(inventory.isLocked(), "Inventory locked") }
}
)
}
@ParameterizedTest(name = "{0}")
@DisplayName("Overflow Compress")
@MethodSource("overflowCompressData")
fun overflowCompress(
@Suppress("UNUSED_PARAMETER") name: String,
inventoryContents: Array<ItemStack?>,
error: String,
invShardCount: Long,
invDiamondCount: Long,
invDiamondBlockCount: Long,
) = runTest {
val inventory = mockPlayerInventory(player, playerUuid, inventoryContents)
val compress = Compress(config, balanceManager, mm, scope)
compress.onCommand(player, command, "compress", arrayOf())
waitForCoroutines(scope)
assertAll(
buildList {
add { verify { player.sendMessage(Component.text("DiamondBank-OG<reset>: <red>$error")) } }
add { assertEquals(invShardCount, inventory.countShards(), "Shard count") }
add { assertEquals(invDiamondCount, inventory.countDiamonds(), "Diamond count") }
add { assertEquals(invDiamondBlockCount, inventory.countDiamondBlocks(), "Diamond block count") }
add { assertFalse(inventory.isLocked(), "Inventory locked") }
}
)
}
@Test
@DisplayName("Compress while in minigame should fail")
fun compressWhileInMinigame() = runTest {
every { player.world.name } returns "minigame_world"
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.DIAMOND, 9)))
val compress = Compress(config, balanceManager, mm, scope)
compress.onCommand(player, command, "compress", arrayOf("all"))
waitForCoroutines(scope)
assertAll(
{
verify {
player.sendMessage(
Component.text("DiamondBank-OG<reset>: <red>You cannot use /compress when in a minigame.")
)
}
},
{ assertEquals(9, inventory.countDiamonds(), "Diamond count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
@Test
@DisplayName("Compress while locked should fail")
fun compressWhileLocked() = runTest {
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.DIAMOND, 9)))
val transactionLock = mockk<TransactionLock>()
coEvery { transactionLock.tryWithLockSuspend<Unit>(any(), any()) } returns TransactionLock.LockResult.Failed
val compress = Compress(config, balanceManager, mm, scope, transactionLock)
compress.onCommand(player, command, "compress", arrayOf("all"))
waitForCoroutines(scope)
assertAll(
{
verify {
player.sendMessage(
Component.text("DiamondBank-OG<reset>: <red>You are currently blocked from using /compress.")
)
}
},
{ assertEquals(9, inventory.countDiamonds(), "Diamond count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
@Test
@DisplayName("Compress too many arguments should fail")
fun compressTooManyArgs() = runTest {
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.DIAMOND, 9)))
val compress = Compress(config, balanceManager, mm, scope)
compress.onCommand(player, command, "compress", arrayOf("1", "2"))
waitForCoroutines(scope)
assertAll(
{
verify {
player.sendMessage(
Component.text(
"DiamondBank-OG<reset>: <red>Do not provide more arguments than \"yes\" if you want to compress the items in the shulker box you're holding."
)
)
}
},
{ assertEquals(9, inventory.countDiamonds(), "Diamond count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
@Test
@DisplayName("Compress with invalid argument should fail")
fun compressInvalidArg() = runTest {
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.DIAMOND, 9)))
val compress = Compress(config, balanceManager, mm, scope)
compress.onCommand(player, command, "compress", arrayOf("invalid"))
waitForCoroutines(scope)
assertAll(
{ verify { player.sendMessage(Component.text("DiamondBank-OG<reset>: <red>Invalid argument.")) } },
{ assertEquals(9, inventory.countDiamonds(), "Diamond count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
@Test
@DisplayName("Compress with no permission should fail")
fun compressNoPermission() = runTest {
every { player.hasPermission("diamondbank-og.compress") } returns false
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.DIAMOND, 9)))
val compress = Compress(config, balanceManager, mm, scope)
compress.onCommand(player, command, "compress", arrayOf("1"))
waitForCoroutines(scope)
assertAll(
{
verify {
player.sendMessage(
Component.text("DiamondBank-OG<reset>: <red>You do not have permission to use this command.")
)
}
},
{ assertEquals(9, inventory.countDiamonds(), "Diamond count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
@Test
@DisplayName("Compress with yes while not holding a shulker box should fail")
fun compressYesNotHoldingShulkerBox() = runTest {
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.DIAMOND, 9)))
val compress = Compress(config, balanceManager, mm, scope)
compress.onCommand(player, command, "compress", arrayOf("yes"))
waitForCoroutines(scope)
assertAll(
{
verify {
player.sendMessage(Component.text("DiamondBank-OG<reset>: <red>You are not holding a shulker box."))
}
},
{ assertEquals(9, inventory.countDiamonds(), "Diamond count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
@Test
@DisplayName("Compress without yes while holding a shulker box should fail")
fun compressNoYesHoldingShulkerBox() = runTest {
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.SHULKER_BOX)))
val compress = Compress(config, balanceManager, mm, scope)
compress.onCommand(player, command, "compress", arrayOf())
waitForCoroutines(scope)
assertAll(
{
verify {
player.sendMessage(
Component.text(
"DiamondBank-OG<reset>: <#FFA500>Are you sure you want to compress the Diamond currency items in the shulker box you're holding? If so, run \"/compress yes\""
)
)
}
},
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
}
@@ -0,0 +1,8 @@
package net.trueog.diamondbankog
import java.util.*
object Constants {
val playerUuid: UUID = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
val otherPlayerUuid: UUID = UUID.fromString("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb")
}
@@ -0,0 +1,397 @@
package net.trueog.diamondbankog
import io.mockk.*
import io.mockk.impl.annotations.MockK
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.test.runTest
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.minimessage.MiniMessage
import net.trueog.diamondbankog.Constants.playerUuid
import net.trueog.diamondbankog.InventoryExtensions.countDiamondBlocks
import net.trueog.diamondbankog.InventoryExtensions.countDiamonds
import net.trueog.diamondbankog.InventoryExtensions.countShards
import net.trueog.diamondbankog.InventoryExtensions.isLocked
import net.trueog.diamondbankog.Utils.mockPlayerInventory
import net.trueog.diamondbankog.Utils.waitForCoroutines
import net.trueog.diamondbankog.commands.Deposit
import org.bukkit.Bukkit
import org.bukkit.Material
import org.bukkit.Server
import org.bukkit.command.Command
import org.bukkit.entity.Player
import org.bukkit.inventory.ItemStack
import org.junit.jupiter.api.*
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.CsvSource
import org.junit.jupiter.params.provider.MethodSource
class DepositTest {
@MockK lateinit var config: Config
@MockK lateinit var balanceManager: BalanceManager
@MockK lateinit var mm: MiniMessage
@MockK lateinit var player: Player
@MockK lateinit var command: Command
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
@BeforeEach
fun setup() {
MockKAnnotations.init(this)
every { config.prefix } returns "DiamondBank-OG"
val inputSlot = slot<String>()
every { mm.deserialize(capture(inputSlot)) } answers { Component.text(inputSlot.captured) }
every { player.uniqueId } returns playerUuid
every { player.world.name } returns "world"
every { player.hasPermission("diamondbank-og.deposit") } returns true
every { Bukkit.getPlayer("player") } returns player
every { server.getPlayer("player") } returns player
every { Bukkit.getPlayer(playerUuid) } returns player
every { server.getPlayer(playerUuid) } returns player
coEvery { balanceManager.addToBankShards(any(), any()) } returns Result.success(Unit)
coEvery { balanceManager.setPlayerShards(any(), any(), PostgreSQL.ShardType.INVENTORY) } returns
Result.success(Unit)
coEvery { balanceManager.insertTransactionLog(any(), any(), any(), any(), any()) } returns Result.success(Unit)
every { player.sendMessage(any<Component>()) } just Runs
}
companion object {
lateinit var server: Server
@JvmStatic
@BeforeAll
fun setupBukkit() {
server = BukkitMock.mockBukkit()
}
@JvmStatic
fun depositData() =
listOf(
Arguments.of(
"1 Diamond",
"1.0 <aqua>Diamond",
arrayOf(ItemStack(Material.DIAMOND, 1)),
"1",
arrayOf<Long>(9),
0,
0,
0,
),
Arguments.of(
"2 Diamonds",
"2.0 <aqua>Diamonds",
arrayOf(ItemStack(Material.DIAMOND, 3)),
"2",
arrayOf<Long>(2 * 9),
0,
1,
0,
),
Arguments.of(
"2.2 Diamonds only diamonds",
"2.2 <aqua>Diamonds",
arrayOf(ItemStack(Material.DIAMOND, 3)),
"2.2",
arrayOf<Long>(20),
7,
0,
0,
),
Arguments.of(
"2.2 Diamonds",
"2.2 <aqua>Diamonds",
arrayOf(Shard.createItemStack(2), ItemStack(Material.DIAMOND, 3)),
"2.2",
arrayOf<Long>(20),
0,
1,
0,
),
Arguments.of(
"2.2 Diamonds with blocks",
"2.2 <aqua>Diamonds",
arrayOf(ItemStack(Material.DIAMOND_BLOCK, 1)),
"2.2",
arrayOf<Long>(20),
7,
6,
0,
),
Arguments.of(
"2.9 Diamonds",
"3.0 <aqua>Diamonds",
arrayOf(ItemStack(Material.DIAMOND, 4)),
"2.9",
arrayOf<Long>(27),
0,
1,
0,
),
Arguments.of(
"all with 5 Diamonds",
"5.0 <aqua>Diamonds",
arrayOf(ItemStack(Material.DIAMOND, 5)),
"all",
arrayOf<Long>(45),
0,
0,
0,
),
Arguments.of(
"all with 5 Diamonds and 8 shards",
"5.8 <aqua>Diamonds",
arrayOf(Shard.createItemStack(8), ItemStack(Material.DIAMOND, 5)),
"all",
arrayOf<Long>(53),
0,
0,
0,
),
Arguments.of(
"1.8 Diamonds with overflow",
"1.8 <aqua>Diamonds",
Array(35) { ItemStack(Material.DIRT, 1) } + arrayOf(ItemStack(Material.DIAMOND, 3)),
"1.8",
arrayOf<Long>(17, 1),
0,
1,
0,
),
Arguments.of(
"1.1 Diamonds with diamond block with overflow",
"1.1 <aqua>Diamonds",
Array(35) { ItemStack(Material.DIRT, 1) } + arrayOf(ItemStack(Material.DIAMOND_BLOCK, 2)),
"1.1",
arrayOf<Long>(10, 63, 8),
0,
0,
1,
),
)
}
@ParameterizedTest(name = "{0}")
@DisplayName("Successful Deposit")
@MethodSource("depositData")
fun successfulDeposit(
@Suppress("UNUSED_PARAMETER") name: String,
successDiamondText: String,
inventoryContents: Array<ItemStack?>,
commandArg: String,
addedShards: Array<Long>,
invShardCount: Long,
invDiamondCount: Long,
invDiamondBlockCount: Long,
) = runTest {
val inventory = mockPlayerInventory(player, playerUuid, inventoryContents)
val deposit = Deposit(config, balanceManager, mm, scope)
deposit.onCommand(player, command, "deposit", arrayOf(commandArg))
waitForCoroutines(scope)
assertAll(
buildList {
add {
verify {
player.sendMessage(
Component.text(
"DiamondBank-OG<reset>: <green>Successfully deposited <yellow>$successDiamondText <green>into your bank account."
)
)
}
}
addedShards.forEach { add { coVerify { balanceManager.addToBankShards(playerUuid, it) } } }
add { assertEquals(invShardCount, inventory.countShards(), "Shard count") }
add { assertEquals(invDiamondCount, inventory.countDiamonds(), "Diamond count") }
add { assertEquals(invDiamondBlockCount, inventory.countDiamondBlocks(), "Diamond block count") }
add { assertFalse(inventory.isLocked(), "Inventory locked") }
}
)
}
@Test
@DisplayName("Deposit 2 Diamonds while only having 1")
fun depositWholeDiamondsWhileShort() = runTest {
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.DIAMOND, 1)))
val deposit = Deposit(config, balanceManager, mm, scope)
deposit.onCommand(player, command, "deposit", arrayOf("2"))
waitForCoroutines(scope)
assertAll(
{
verify {
player.sendMessage(
Component.text(
"DiamondBank-OG<reset>: <red>You do not have <yellow>2.0 <aqua>Diamonds <red>to deposit."
)
)
}
},
{ coVerify(exactly = 0) { balanceManager.addToBankShards(any(), any()) } },
{ assertEquals(1, inventory.countDiamonds(), "Diamond count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
@Test
@DisplayName("Deposit while in minigame should fail")
fun depositWhileInMinigame() = runTest {
every { player.world.name } returns "minigame_world"
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.DIAMOND, 5)))
val deposit = Deposit(config, balanceManager, mm, scope)
deposit.onCommand(player, command, "deposit", arrayOf("all"))
waitForCoroutines(scope)
assertAll(
{
verify {
player.sendMessage(
Component.text("DiamondBank-OG<reset>: <red>You cannot use /deposit when in a minigame.")
)
}
},
{ coVerify(exactly = 0) { balanceManager.addToBankShards(any(), any()) } },
{ assertEquals(5, inventory.countDiamonds(), "Diamond count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
@Test
@DisplayName("Deposit while locked should fail")
fun depositWhileLocked() = runTest {
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.DIAMOND, 5)))
val transactionLock = mockk<TransactionLock>()
coEvery { transactionLock.tryWithLockSuspend<Unit>(any(), any()) } returns TransactionLock.LockResult.Failed
val deposit = Deposit(config, balanceManager, mm, scope, transactionLock)
deposit.onCommand(player, command, "deposit", arrayOf("all"))
waitForCoroutines(scope)
assertAll(
{
verify {
player.sendMessage(
Component.text("DiamondBank-OG<reset>: <red>You are currently blocked from using /deposit.")
)
}
},
{ coVerify(exactly = 0) { balanceManager.addToBankShards(any(), any()) } },
{ assertEquals(5, inventory.countDiamonds(), "Diamond count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
@Test
@DisplayName("Deposit no arguments should fail")
fun depositNoArgs() = runTest {
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.DIAMOND, 5)))
val deposit = Deposit(config, balanceManager, mm, scope)
deposit.onCommand(player, command, "deposit", arrayOf())
waitForCoroutines(scope)
assertAll(
{
verify {
player.sendMessage(
Component.text(
"DiamondBank-OG<reset>: <red>You did not provide the amount of <aqua>Diamonds <red>that you want to deposit."
)
)
}
},
{ coVerify(exactly = 0) { balanceManager.addToBankShards(any(), any()) } },
{ assertEquals(5, inventory.countDiamonds(), "Diamond count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
@Test
@DisplayName("Deposit too many arguments should fail")
fun depositTooManyArgs() = runTest {
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.DIAMOND, 5)))
val deposit = Deposit(config, balanceManager, mm, scope)
deposit.onCommand(player, command, "deposit", arrayOf("1", "2"))
waitForCoroutines(scope)
assertAll(
{
verify {
player.sendMessage(
Component.text(
"DiamondBank-OG<reset>: <red>Please (only) provide the amount of <aqua>Diamonds <red>you want to deposit. Either a number or \"all\"."
)
)
}
},
{ coVerify(exactly = 0) { balanceManager.addToBankShards(any(), any()) } },
{ assertEquals(5, inventory.countDiamonds(), "Diamond count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
@ParameterizedTest(name = "{0}")
@DisplayName("Deposit with invalid argument should fail")
@CsvSource(
"Not a number, abc, Invalid argument",
"Negative, -1, You cannot deposit a negative or zero amount",
"Zero, 0, You cannot deposit a negative or zero amount",
"More than one decimal digit, 1.11, <aqua>Diamonds<red> can only have one decimal digit. Issue /diamondbankhelp for more information",
)
fun depositInvalidArg(@Suppress("UNUSED_PARAMETER") name: String, argument: String, errorMessage: String) =
runTest {
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.DIAMOND, 5)))
val deposit = Deposit(config, balanceManager, mm, scope)
deposit.onCommand(player, command, "deposit", arrayOf(argument))
waitForCoroutines(scope)
assertAll(
{ verify { player.sendMessage(Component.text("DiamondBank-OG<reset>: <red>$errorMessage.")) } },
{ coVerify(exactly = 0) { balanceManager.addToBankShards(any(), any()) } },
{ assertEquals(5, inventory.countDiamonds(), "Diamond count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
@Test
@DisplayName("Deposit with no permission should fail")
fun depositNoPermission() = runTest {
every { player.hasPermission("diamondbank-og.deposit") } returns false
val inventory = mockPlayerInventory(player, playerUuid, arrayOf(ItemStack(Material.DIAMOND, 5)))
val deposit = Deposit(config, balanceManager, mm, scope)
deposit.onCommand(player, command, "deposit", arrayOf("1"))
waitForCoroutines(scope)
assertAll(
{
verify {
player.sendMessage(
Component.text("DiamondBank-OG<reset>: <red>You do not have permission to use this command.")
)
}
},
{ coVerify(exactly = 0) { balanceManager.addToBankShards(any(), any()) } },
{ assertEquals(5, inventory.countDiamonds(), "Diamond count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
}
@@ -0,0 +1,475 @@
package net.trueog.diamondbankog
import io.mockk.*
import io.mockk.impl.annotations.MockK
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.test.runTest
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.minimessage.MiniMessage
import net.luckperms.api.LuckPerms
import net.luckperms.api.model.user.User
import net.luckperms.api.node.Node
import net.trueog.diamondbankog.Constants.otherPlayerUuid
import net.trueog.diamondbankog.Constants.playerUuid
import net.trueog.diamondbankog.InventoryExtensions.countDiamondBlocks
import net.trueog.diamondbankog.InventoryExtensions.countDiamonds
import net.trueog.diamondbankog.InventoryExtensions.countShards
import net.trueog.diamondbankog.InventoryExtensions.countTotal
import net.trueog.diamondbankog.InventoryExtensions.isLocked
import net.trueog.diamondbankog.Utils.mockPlayerInventory
import net.trueog.diamondbankog.Utils.waitForCoroutines
import net.trueog.diamondbankog.commands.Pay
import org.bukkit.Bukkit
import org.bukkit.Material
import org.bukkit.Server
import org.bukkit.command.Command
import org.bukkit.entity.Player
import org.bukkit.inventory.ItemStack
import org.junit.jupiter.api.*
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.CsvSource
import org.junit.jupiter.params.provider.MethodSource
class PayTest {
@MockK lateinit var config: Config
@MockK lateinit var balanceManager: BalanceManager
@MockK lateinit var mm: MiniMessage
@MockK lateinit var player: Player
@MockK lateinit var otherPlayer: Player
@MockK lateinit var command: Command
@MockK lateinit var luckPerms: LuckPerms
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
@BeforeEach
fun setup() {
MockKAnnotations.init(this)
every { config.prefix } returns "DiamondBank-OG"
val inputSlot = slot<String>()
every { mm.deserialize(capture(inputSlot)) } answers { Component.text(inputSlot.captured) }
every { player.uniqueId } returns playerUuid
every { player.name } returns "Player"
every { player.world.name } returns "world"
every { player.hasPermission("diamondbank-og.pay") } returns true
every { otherPlayer.uniqueId } returns otherPlayerUuid
every { otherPlayer.name } returns "OtherPlayer"
every { otherPlayer.isOnline } returns true
every { otherPlayer.player } returns otherPlayer
every { Bukkit.getPlayer("player") } returns player
every { server.getPlayer("player") } returns player
every { Bukkit.getPlayer(playerUuid) } returns player
every { server.getPlayer(playerUuid) } returns player
every { Bukkit.getPlayer("otherplayer") } returns otherPlayer
every { server.getPlayer("otherplayer") } returns otherPlayer
every { Bukkit.getPlayer(otherPlayerUuid) } returns otherPlayer
every { server.getPlayer(otherPlayerUuid) } returns otherPlayer
every { otherPlayer.hasPlayedBefore() } returns true
coEvery { balanceManager.addToBankShards(any(), any()) } returns Result.success(Unit)
coEvery { balanceManager.setPlayerShards(any(), any(), PostgreSQL.ShardType.INVENTORY) } returns
Result.success(Unit)
coEvery { balanceManager.subtractFromBankShards(any(), any()) } returns Result.success(Unit)
coEvery { balanceManager.insertTransactionLog(any(), any(), any(), any(), any()) } returns Result.success(Unit)
every { player.sendMessage(any<Component>()) } just Runs
every { otherPlayer.sendMessage(any<Component>()) } just Runs
}
companion object {
lateinit var server: Server
@JvmStatic
@BeforeAll
fun setupBukkit() {
server = BukkitMock.mockBukkit()
}
@JvmStatic
fun payData() =
listOf(
Arguments.of(
"1 Diamond from inventory",
"1.0 <aqua>Diamond",
arrayOf(ItemStack(Material.DIAMOND, 1)),
0,
-1,
"1",
arrayOf<Long>(),
arrayOf<Long>(9),
0,
0,
0,
),
Arguments.of(
"2 Diamonds from inventory",
"2.0 <aqua>Diamonds",
arrayOf(ItemStack(Material.DIAMOND, 3)),
0,
-1,
"2",
arrayOf<Long>(),
arrayOf<Long>(2 * 9),
0,
1,
0,
),
Arguments.of(
"2.2 Diamonds from inventory only diamonds",
"2.2 <aqua>Diamonds",
arrayOf(ItemStack(Material.DIAMOND, 3)),
0,
-1,
"2.2",
arrayOf<Long>(),
arrayOf<Long>(20),
7,
0,
0,
),
Arguments.of(
"2.2 Diamonds from inventory",
"2.2 <aqua>Diamonds",
arrayOf(Shard.createItemStack(2), ItemStack(Material.DIAMOND, 3)),
0,
-1,
"2.2",
arrayOf<Long>(),
arrayOf<Long>(20),
0,
1,
0,
),
Arguments.of(
"2.2 Diamonds from inventory with blocks",
"2.2 <aqua>Diamonds",
arrayOf(ItemStack(Material.DIAMOND_BLOCK, 1)),
0,
-1,
"2.2",
arrayOf<Long>(),
arrayOf<Long>(20),
7,
6,
0,
),
Arguments.of(
"2.9 Diamonds from inventory",
"3.0 <aqua>Diamonds",
arrayOf(ItemStack(Material.DIAMOND, 4)),
0,
-1,
"2.9",
arrayOf<Long>(),
arrayOf<Long>(27),
0,
1,
0,
),
Arguments.of(
"1.8 Diamonds from inventory with overflow",
"1.8 <aqua>Diamonds",
Array(35) { ItemStack(Material.DIRT, 1) } + arrayOf(ItemStack(Material.DIAMOND, 3)),
0,
-1,
"1.8",
arrayOf<Long>(1),
arrayOf<Long>(17),
0,
1,
0,
),
Arguments.of(
"1.1 Diamonds from inventory with diamond block with overflow",
"1.1 <aqua>Diamonds",
Array(35) { ItemStack(Material.DIRT, 1) } + arrayOf(ItemStack(Material.DIAMOND_BLOCK, 2)),
0,
-1,
"1.1",
arrayOf<Long>(63, 8),
arrayOf<Long>(10),
0,
0,
1,
),
Arguments.of(
"1 Diamond: 0.5 Diamonds from bank and 1 Diamond in inventory",
"1.0 <aqua>Diamond",
arrayOf(ItemStack(Material.DIAMOND, 1)),
5,
5,
"1",
arrayOf<Long>(),
arrayOf<Long>(9),
5,
0,
0,
),
Arguments.of(
"5 Diamonds from bank",
"5.0 <aqua>Diamonds",
arrayOf<ItemStack>(),
45,
45,
"5",
arrayOf<Long>(),
arrayOf<Long>(45),
0,
0,
0,
),
)
}
@ParameterizedTest(name = "{0}")
@DisplayName("Successful Pay")
@MethodSource("payData")
fun successfulPay(
@Suppress("UNUSED_PARAMETER") name: String,
successDiamondText: String,
inventoryContents: Array<ItemStack?>,
bankShards: Long,
bankShardsRemoved: Long,
commandArg: String,
addedShardsPlayer: Array<Long>,
addedShardsOtherPlayer: Array<Long>,
invShardCount: Long,
invDiamondCount: Long,
invDiamondBlockCount: Long,
) = runTest {
val inventory = mockPlayerInventory(player, playerUuid, inventoryContents)
coEvery { balanceManager.getBankShards(playerUuid) } returns Result.success(bankShards)
val user = mockk<User>()
every { user.nodes } returns mutableListOf<Node>()
every { luckPerms.userManager.getUser(any<UUID>()) } returns user
val pay = Pay(config, balanceManager, mm, scope, luckPerms)
pay.onCommand(player, command, "pay", arrayOf("otherplayer", commandArg))
waitForCoroutines(scope)
assertAll(
buildList {
add {
verify {
player.sendMessage(
Component.text(
"DiamondBank-OG<reset>: <green>Successfully paid <yellow>$successDiamondText <green>to OtherPlayer<reset><green>."
)
)
}
}
add {
verify {
otherPlayer.sendMessage(
Component.text(
"DiamondBank-OG<reset>: <green> Player<reset> <green>has paid you <yellow>$successDiamondText<green>."
)
)
}
}
add {
if (bankShardsRemoved != -1L)
coVerify { balanceManager.subtractFromBankShards(playerUuid, bankShardsRemoved) }
}
addedShardsPlayer.forEach { add { coVerify { balanceManager.addToBankShards(playerUuid, it) } } }
addedShardsOtherPlayer.forEach {
add { coVerify { balanceManager.addToBankShards(otherPlayerUuid, it) } }
}
add { assertEquals(invShardCount, inventory.countShards(), "Shard count") }
add { assertEquals(invDiamondCount, inventory.countDiamonds(), "Diamond count") }
add { assertEquals(invDiamondBlockCount, inventory.countDiamondBlocks(), "Diamond block count") }
add { assertFalse(inventory.isLocked(), "Inventory locked") }
}
)
}
@Test
@DisplayName("Pay while in minigame should fail")
fun payWhileInMinigame() = runTest {
every { player.world.name } returns "minigame_world"
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
val pay = Pay(config, balanceManager, mm, scope, luckPerms)
pay.onCommand(player, command, "pay", arrayOf("otherplayer", "1"))
waitForCoroutines(scope)
assertAll(
{
verify {
player.sendMessage(
Component.text("DiamondBank-OG<reset>: <red>You cannot use /pay when in a minigame.")
)
}
},
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
{ assertEquals(0, inventory.countTotal(), "Total count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
@Test
@DisplayName("Pay while locked should fail")
fun payWhileLocked() = runTest {
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
val transactionLock = mockk<TransactionLock>()
coEvery { transactionLock.tryWithLockSuspend<Unit>(any(), any()) } returns TransactionLock.LockResult.Failed
val pay = Pay(config, balanceManager, mm, scope, luckPerms, transactionLock)
pay.onCommand(player, command, "pay", arrayOf("otherplayer", "1"))
waitForCoroutines(scope)
assertAll(
{
verify {
player.sendMessage(
Component.text("DiamondBank-OG<reset>: <red>You are currently blocked from using /pay.")
)
}
},
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
{ assertEquals(0, inventory.countTotal(), "Total count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
@Test
@DisplayName("Pay no arguments should fail")
fun payNoArgs() = runTest {
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
val pay = Pay(config, balanceManager, mm, scope, luckPerms)
pay.onCommand(player, command, "pay", arrayOf())
waitForCoroutines(scope)
assertAll(
{
verify {
player.sendMessage(
Component.text(
"DiamondBank-OG<reset>: <red>You did not provide the name or the UUID of a player and the amount of <aqua>Diamonds<red>."
)
)
}
},
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
{ assertEquals(0, inventory.countTotal(), "Total count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
@Test
@DisplayName("Pay too few arguments should fail")
fun payTooFewArgs() = runTest {
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
val pay = Pay(config, balanceManager, mm, scope, luckPerms)
pay.onCommand(player, command, "pay", arrayOf("1"))
waitForCoroutines(scope)
assertAll(
{
verify {
player.sendMessage(
Component.text(
"DiamondBank-OG<reset>: <red>Please (only) provide the name or the UUID of a player and the amount of <aqua>Diamonds<red>."
)
)
}
},
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
{ assertEquals(0, inventory.countTotal(), "Total count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
@Test
@DisplayName("Pay too many arguments should fail")
fun payTooManyArgs() = runTest {
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
val pay = Pay(config, balanceManager, mm, scope, luckPerms)
pay.onCommand(player, command, "pay", arrayOf("1", "2", "3"))
waitForCoroutines(scope)
assertAll(
{
verify {
player.sendMessage(
Component.text(
"DiamondBank-OG<reset>: <red>Please (only) provide the name or the UUID of a player and the amount of <aqua>Diamonds<red>."
)
)
}
},
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
{ assertEquals(0, inventory.countTotal(), "Total count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
@ParameterizedTest(name = "{0}")
@DisplayName("Pay with invalid argument should fail")
@CsvSource(
"Not a number, abc, Invalid argument",
"Negative, -1, You cannot pay a negative or zero amount",
"Zero, 0, You cannot pay a negative or zero amount",
"More than one decimal digit, 1.11, <aqua>Diamonds<red> can only have one decimal digit. Issue /diamondbankhelp for more information",
)
fun payInvalidArg(@Suppress("UNUSED_PARAMETER") name: String, argument: String, errorMessage: String) = runTest {
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
val pay = Pay(config, balanceManager, mm, scope, luckPerms)
pay.onCommand(player, command, "pay", arrayOf("otherplayer", argument))
waitForCoroutines(scope)
assertAll(
{ verify { player.sendMessage(Component.text("DiamondBank-OG<reset>: <red>$errorMessage.")) } },
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
{ assertEquals(0, inventory.countTotal(), "Total count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
@Test
@DisplayName("Pay with no permission should fail")
fun payNoPermission() = runTest {
every { player.hasPermission("diamondbank-og.pay") } returns false
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
val pay = Pay(config, balanceManager, mm, scope, luckPerms)
pay.onCommand(player, command, "pay", arrayOf("1"))
waitForCoroutines(scope)
assertAll(
{
verify {
player.sendMessage(
Component.text("DiamondBank-OG<reset>: <red>You do not have permission to use this command.")
)
}
},
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
{ assertEquals(0, inventory.countTotal(), "Total count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
}
@@ -0,0 +1,38 @@
package net.trueog.diamondbankog
import io.mockk.every
import io.mockk.mockk
import java.util.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import org.bukkit.Material
import org.bukkit.entity.Player
import org.bukkit.inventory.ItemStack
import org.bukkit.inventory.PlayerInventory
object Utils {
suspend fun waitForCoroutines(scope: CoroutineScope) {
scope.coroutineContext[Job]?.children?.forEach { it.join() }
}
fun allImpl(contents: Array<ItemStack?>, material: Material): HashMap<Int, out ItemStack> =
HashMap(
contents.mapIndexedNotNull { index, item -> if (item?.type == material) index to item else null }.toMap()
)
fun mockPlayerInventory(player: Player, uuid: UUID, contents: Array<ItemStack?>): PlayerInventory {
val playerInventory = mockk<PlayerInventory>()
var contents = contents.copyOf(36)
every { playerInventory.contents } answers { contents }
every { playerInventory.storageContents = any() } answers { contents = firstArg() }
every { playerInventory.all(any<Material>()) } answers
{
val material = firstArg<Material>()
allImpl(contents, material)
}
every { playerInventory.heldItemSlot } returns 0
every { playerInventory.holder!!.uniqueId } returns uuid
every { player.inventory } returns playerInventory
return playerInventory
}
}
@@ -0,0 +1,317 @@
package net.trueog.diamondbankog
import io.mockk.*
import io.mockk.impl.annotations.MockK
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.test.runTest
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.minimessage.MiniMessage
import net.trueog.diamondbankog.Constants.playerUuid
import net.trueog.diamondbankog.InventoryExtensions.countDiamonds
import net.trueog.diamondbankog.InventoryExtensions.countShards
import net.trueog.diamondbankog.InventoryExtensions.countTotal
import net.trueog.diamondbankog.InventoryExtensions.isLocked
import net.trueog.diamondbankog.Utils.mockPlayerInventory
import net.trueog.diamondbankog.Utils.waitForCoroutines
import net.trueog.diamondbankog.commands.Withdraw
import org.bukkit.Bukkit
import org.bukkit.Material
import org.bukkit.Server
import org.bukkit.command.Command
import org.bukkit.entity.Player
import org.bukkit.inventory.ItemStack
import org.junit.jupiter.api.*
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
class WithdrawTest {
@MockK lateinit var config: Config
@MockK lateinit var balanceManager: BalanceManager
@MockK lateinit var mm: MiniMessage
@MockK lateinit var player: Player
@MockK lateinit var command: Command
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
@BeforeEach
fun setup() {
MockKAnnotations.init(this)
every { config.prefix } returns "DiamondBank-OG"
val inputSlot = slot<String>()
every { mm.deserialize(capture(inputSlot)) } answers { Component.text(inputSlot.captured) }
every { player.uniqueId } returns playerUuid
every { player.world.name } returns "world"
every { player.hasPermission("diamondbank-og.withdraw") } returns true
every { Bukkit.getPlayer("player") } returns player
every { server.getPlayer("player") } returns player
coEvery { balanceManager.addToBankShards(any(), any()) } returns Result.success(Unit)
coEvery { balanceManager.setPlayerShards(any(), any(), PostgreSQL.ShardType.INVENTORY) } returns
Result.success(Unit)
coEvery { balanceManager.subtractFromBankShards(any(), any()) } returns Result.success(Unit)
coEvery { balanceManager.insertTransactionLog(any(), any(), any(), any(), any()) } returns Result.success(Unit)
every { player.sendMessage(any<Component>()) } just Runs
}
companion object {
lateinit var server: Server
@JvmStatic
@BeforeAll
fun setupBukkit() {
server = BukkitMock.mockBukkit()
}
}
@ParameterizedTest(name = "{0}")
@DisplayName("Successful withdraw")
@CsvSource(
"2 Diamonds, 2.0 <aqua>Diamonds, 2, 0, 2, 18",
"2.2 Diamonds, 2.2 <aqua>Diamonds, 2.2, 2, 2, 20",
"2.9 Diamonds, 3.0 <aqua>Diamonds, 2.9, 0, 3, 27",
"All, 11.1 <aqua>Diamonds, all, 1, 11, 100",
)
fun successfulWithdraw(
@Suppress("UNUSED_PARAMETER") name: String,
successDiamondText: String,
commandArg: String,
invShardsCount: Long,
invDiamondsCount: Long,
removedShardCount: Long,
) = runTest {
coEvery { balanceManager.getBankShards(playerUuid) } returns Result.success(100)
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
val withdraw = Withdraw(config, balanceManager, mm, scope)
withdraw.onCommand(player, command, "withdraw", arrayOf(commandArg))
waitForCoroutines(scope)
assertAll(
{
verify {
player.sendMessage(
Component.text(
"DiamondBank-OG<reset>: <green>Successfully withdrew <yellow>$successDiamondText <green>from your bank account."
)
)
}
},
{ coVerify { balanceManager.subtractFromBankShards(playerUuid, removedShardCount) } },
{ assertEquals(invShardsCount, inventory.countShards(), "Shard count") },
{ assertEquals(invDiamondsCount, inventory.countDiamonds(), "Diamond count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
@Test
@DisplayName("Withdraw while in minigame should fail")
fun withdrawWhileInMinigame() = runTest {
every { player.world.name } returns "minigame_world"
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
val withdraw = Withdraw(config, balanceManager, mm, scope)
withdraw.onCommand(player, command, "withdraw", arrayOf("all"))
waitForCoroutines(scope)
assertAll(
{
verify {
player.sendMessage(
Component.text("DiamondBank-OG<reset>: <red>You cannot use /withdraw when in a minigame.")
)
}
},
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
{ assertEquals(0, inventory.countTotal(), "Total count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
@Test
@DisplayName("Withdraw while locked should fail")
fun withdrawWhileLocked() = runTest {
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
val transactionLock = mockk<TransactionLock>()
coEvery { transactionLock.tryWithLockSuspend<Unit>(any(), any()) } returns TransactionLock.LockResult.Failed
val withdraw = Withdraw(config, balanceManager, mm, scope, transactionLock)
withdraw.onCommand(player, command, "withdraw", arrayOf("all"))
waitForCoroutines(scope)
assertAll(
{
verify {
player.sendMessage(
Component.text("DiamondBank-OG<reset>: <red>You are currently blocked from using /withdraw.")
)
}
},
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
{ assertEquals(0, inventory.countTotal(), "Total count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
@Test
@DisplayName("Withdraw no arguments should fail")
fun withdrawNoArgs() = runTest {
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
val withdraw = Withdraw(config, balanceManager, mm, scope)
withdraw.onCommand(player, command, "withdraw", arrayOf())
waitForCoroutines(scope)
assertAll(
{
verify {
player.sendMessage(
Component.text(
"DiamondBank-OG<reset>: <red>You did not provide the amount of <aqua>Diamonds <red>that you want to withdraw."
)
)
}
},
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
{ assertEquals(0, inventory.countTotal(), "Total count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
@Test
@DisplayName("Withdraw too many arguments should fail")
fun withdrawTooManyArgs() = runTest {
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
val withdraw = Withdraw(config, balanceManager, mm, scope)
withdraw.onCommand(player, command, "withdraw", arrayOf("1", "2"))
waitForCoroutines(scope)
assertAll(
{
verify {
player.sendMessage(
Component.text(
"DiamondBank-OG<reset>: <red>Please (only) provide the amount of <aqua>Diamonds <red>you want to withdraw. Either a number or \"all\"."
)
)
}
},
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
{ assertEquals(0, inventory.countTotal(), "Total count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
@ParameterizedTest(name = "{0}")
@DisplayName("Withdraw with invalid argument should fail")
@CsvSource(
"Not a number, abc, Invalid argument",
"Negative, -1, You cannot withdraw a negative or zero amount",
"Zero, 0, You cannot withdraw a negative or zero amount",
"More than one decimal digit, 1.11, <aqua>Diamonds<red> can only have one decimal digit. Issue /diamondbankhelp for more information",
)
fun withdrawInvalidArg(@Suppress("UNUSED_PARAMETER") name: String, argument: String, errorMessage: String) =
runTest {
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
val withdraw = Withdraw(config, balanceManager, mm, scope)
withdraw.onCommand(player, command, "withdraw", arrayOf(argument))
waitForCoroutines(scope)
assertAll(
{ verify { player.sendMessage(Component.text("DiamondBank-OG<reset>: <red>$errorMessage.")) } },
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
{ assertEquals(0, inventory.countTotal(), "Total count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
@Test
@DisplayName("Withdraw with no permission should fail")
fun withdrawNoPermission() = runTest {
every { player.hasPermission("diamondbank-og.withdraw") } returns false
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
val withdraw = Withdraw(config, balanceManager, mm, scope)
withdraw.onCommand(player, command, "withdraw", arrayOf("1"))
waitForCoroutines(scope)
assertAll(
{
verify {
player.sendMessage(
Component.text("DiamondBank-OG<reset>: <red>You do not have permission to use this command.")
)
}
},
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
{ assertEquals(0, inventory.countTotal(), "Total count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
@Test
@DisplayName("Withdraw when not enough Diamonds in bank should fail")
fun withdrawNotEnoughDiamondsInBank() = runTest {
coEvery { balanceManager.getBankShards(playerUuid) } returns Result.success(18)
val inventory = mockPlayerInventory(player, playerUuid, emptyArray())
val withdraw = Withdraw(config, balanceManager, mm, scope)
withdraw.onCommand(player, command, "withdraw", arrayOf("5"))
waitForCoroutines(scope)
assertAll(
{
verify {
player.sendMessage(
Component.text(
"DiamondBank-OG<reset>: <red>Cannot withdraw <yellow>5.0 <aqua>Diamonds <red>because your bank only contains <yellow><yellow>2.0 <aqua>Diamonds<red>."
)
)
}
},
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
{ assertEquals(0, inventory.countTotal(), "Total count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
@Test
@DisplayName("Withdraw when not enough inventory space should fail")
fun withdrawNotEnoughInventorySpace() = runTest {
coEvery { balanceManager.getBankShards(playerUuid) } returns Result.success(18)
val inventory = mockPlayerInventory(player, playerUuid, Array(36) { ItemStack(Material.DIRT, 1) })
val withdraw = Withdraw(config, balanceManager, mm, scope)
withdraw.onCommand(player, command, "withdraw", arrayOf("2"))
waitForCoroutines(scope)
assertAll(
{
verify {
player.sendMessage(
Component.text(
"DiamondBank-OG<reset>: <red>You don't have enough inventory space to withdraw <yellow>2.0 <aqua>Diamonds<red>."
)
)
}
},
{ coVerify(exactly = 0) { balanceManager.subtractFromBankShards(any(), any()) } },
{ assertEquals(0, inventory.countTotal(), "Total count") },
{ assertFalse(inventory.isLocked(), "Inventory locked") },
)
}
}