diff --git a/gradle.properties b/gradle.properties index 1034f55..3d0715b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ makeevrserg.java.ktarget=21 # Project makeevrserg.project.name=AspeKt makeevrserg.project.group=ru.astrainteractive.aspekt -makeevrserg.project.version.string=2.25.2 +makeevrserg.project.version.string=2.26.0 makeevrserg.project.description=Essentials plugin for EmpireProjekt makeevrserg.project.developers=makeevrserg|Makeev Roman|makeevrserg@gmail.com makeevrserg.project.url=https://empireprojekt.ru diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d6cd051..85095b4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -107,7 +107,7 @@ minecraft-astralibs-command-bukkit = { module = "ru.astrainteractive.astralibs:c [bundles] exposed = ["exposed-java-time", "exposed-jdbc", "exposed-dao", "exposed-core"] minecraft-bukkit = ["minecraft-paper-api", "minecraft-spigot-api", "minecraft-spigot-core", "minecraft-vaultapi", "minecraft-papi"] -testing-kotlin = ["kotlin-coroutines-core", "kotlin-coroutines-coreJvm", "driver-jdbc", "driver-mysql", "kotlin-serialization", "kotlin-serializationJson"] +testing-kotlin = ["kotlin-coroutines-core", "kotlin-coroutines-coreJvm", "kotlin-coroutines-test", "driver-jdbc", "driver-mysql", "kotlin-serialization", "kotlin-serializationJson"] kotlin = ["kotlin-coroutines-core", "kotlin-coroutines-coreJvm", "kotlin-serialization", "kotlin-serializationJson", "kotlin-serializationKaml", "kotlin-gradle"] diff --git a/modules/moneydrop/build.gradle.kts b/modules/moneydrop/build.gradle.kts index f28ad30..012c356 100644 --- a/modules/moneydrop/build.gradle.kts +++ b/modules/moneydrop/build.gradle.kts @@ -6,6 +6,7 @@ plugins { dependencies { // Kotlin implementation(libs.bundles.kotlin) + implementation(libs.bundles.exposed) // Spigot dependencies compileOnly(libs.minecraft.paper.api) implementation(libs.minecraft.bstats) @@ -13,6 +14,7 @@ dependencies { implementation(libs.minecraft.astralibs.core) implementation(libs.minecraft.astralibs.orm) implementation(libs.klibs.kdi) + implementation(libs.klibs.mikro.core) implementation(libs.minecraft.astralibs.menu.bukkit) implementation(libs.minecraft.astralibs.core.bukkit) implementation(libs.minecraft.vaultapi) diff --git a/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/MoneyDropController.kt b/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/MoneyDropController.kt index aa2c161..69006de 100644 --- a/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/MoneyDropController.kt +++ b/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/MoneyDropController.kt @@ -1,46 +1,51 @@ package ru.astrainteractive.aspekt.module.moneydrop +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.bukkit.Location import org.bukkit.Material import org.bukkit.inventory.ItemStack -import org.jetbrains.kotlin.com.google.common.cache.Cache -import org.jetbrains.kotlin.com.google.common.cache.CacheBuilder +import ru.astrainteractive.aspekt.module.moneydrop.database.dao.MoneyDropDao +import ru.astrainteractive.aspekt.module.moneydrop.database.model.MoneyDropLocation import ru.astrainteractive.aspekt.plugin.PluginConfiguration import ru.astrainteractive.aspekt.plugin.PluginTranslation +import ru.astrainteractive.astralibs.async.AsyncComponent import ru.astrainteractive.astralibs.kyori.KyoriComponentSerializer import ru.astrainteractive.astralibs.persistence.Persistence.getPersistentData import ru.astrainteractive.astralibs.persistence.Persistence.hasPersistentData import ru.astrainteractive.astralibs.persistence.Persistence.setPersistentDataType import ru.astrainteractive.klibs.kdi.Dependency import ru.astrainteractive.klibs.kdi.getValue -import java.util.concurrent.TimeUnit +import ru.astrainteractive.klibs.mikro.core.dispatchers.KotlinDispatchers import kotlin.random.Random -class MoneyDropController( +internal class MoneyDropController( pluginConfigurationDependency: Dependency, translationDependency: Dependency, - kyoriComponentSerializerDependency: Dependency -) { + kyoriComponentSerializerDependency: Dependency, + private val dao: MoneyDropDao, + private val dispatchers: KotlinDispatchers +) : AsyncComponent() { private val pluginConfiguration by pluginConfigurationDependency private val translation by translationDependency private val kyoriComponentSerializer by kyoriComponentSerializerDependency - private val dropCache: Cache = CacheBuilder - .newBuilder() - .maximumSize(64) - .expireAfterWrite(30, TimeUnit.SECONDS) - .build() - - private fun Location.toKeyLocation() = "${x.toInt()}${y.toInt()}${z.toInt()}" + private fun Location.toMoneyDropLocation(additionalConstraint: String?) = MoneyDropLocation( + x = this.x.toInt(), + y = this.y.toInt(), + z = this.z.toInt(), + world = this.world.name, + additionalConstraint = additionalConstraint + ) private fun checkForChance(entry: PluginConfiguration.MoneyDropEntry): Boolean { val chance = entry.chance return chance > Random.nextDouble(0.0, 100.0) } - private fun drop(location: Location, entry: PluginConfiguration.MoneyDropEntry) { - if (dropCache.getIfPresent(location.toKeyLocation()) != null) return - dropCache.put(location.toKeyLocation(), Unit) + private suspend fun drop(location: Location, entry: PluginConfiguration.MoneyDropEntry) { + if (dao.isLocationExists(location.toMoneyDropLocation(entry.from))) return + rememberLocation(location, entry.from) val amount = Random.nextDouble(entry.min, entry.max) val material = Material.RAW_GOLD @@ -51,7 +56,7 @@ class MoneyDropController( it.setPersistentDataType(MoneyDropFlag.Flag, true) it.setPersistentDataType(MoneyDropFlag.Amount, amount) } - val item = location.world.dropItemNaturally(location, itemStack) + val item = withContext(dispatchers.Main) { location.world.dropItemNaturally(location, itemStack) } item.customName(name) item.isCustomNameVisible = true } @@ -65,14 +70,14 @@ class MoneyDropController( return itemStack.itemMeta.getPersistentData(MoneyDropFlag.Amount) } - fun tryDrop(location: Location, from: String) { + fun tryDrop(location: Location, from: String) = launch { pluginConfiguration.moneyDrop.values .filter { it.from == from } .filter(::checkForChance) .forEach { entry -> drop(location, entry) } } - fun blockPlaced(location: Location) { - dropCache.put(location.toKeyLocation(), Unit) + fun rememberLocation(location: Location, additionalConstraint: String? = null) { + launch { dao.addLocation(location.toMoneyDropLocation(additionalConstraint)) } } } diff --git a/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/MoneyDropEvent.kt b/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/MoneyDropEvent.kt index a565514..3f173f3 100644 --- a/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/MoneyDropEvent.kt +++ b/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/MoneyDropEvent.kt @@ -12,7 +12,7 @@ import org.bukkit.event.inventory.InventoryPickupItemEvent import ru.astrainteractive.aspekt.module.moneydrop.di.MoneyDropDependencies import ru.astrainteractive.astralibs.event.DSLEvent -class MoneyDropEvent( +internal class MoneyDropEvent( dependencies: MoneyDropDependencies ) : MoneyDropDependencies by dependencies { @@ -55,6 +55,6 @@ class MoneyDropEvent( } val blockPlaceEvent = DSLEvent(eventListener, plugin) { e -> - moneyDropController.blockPlaced(e.blockPlaced.location) + moneyDropController.rememberLocation(e.blockPlaced.location) } } diff --git a/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/MoneyDropFlag.kt b/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/MoneyDropFlag.kt index 1e47048..8add8bf 100644 --- a/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/MoneyDropFlag.kt +++ b/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/MoneyDropFlag.kt @@ -3,7 +3,7 @@ package ru.astrainteractive.aspekt.module.moneydrop import org.bukkit.persistence.PersistentDataType import ru.astrainteractive.astralibs.persistence.BukkitConstant -object MoneyDropFlag { +internal object MoneyDropFlag { /** * Amount of money will be added */ diff --git a/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/database/dao/MoneyDropDao.kt b/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/database/dao/MoneyDropDao.kt new file mode 100644 index 0000000..318e805 --- /dev/null +++ b/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/database/dao/MoneyDropDao.kt @@ -0,0 +1,8 @@ +package ru.astrainteractive.aspekt.module.moneydrop.database.dao + +import ru.astrainteractive.aspekt.module.moneydrop.database.model.MoneyDropLocation + +internal interface MoneyDropDao { + suspend fun addLocation(location: MoneyDropLocation) + suspend fun isLocationExists(location: MoneyDropLocation): Boolean +} diff --git a/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/database/dao/impl/MoneyDropDaoImpl.kt b/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/database/dao/impl/MoneyDropDaoImpl.kt new file mode 100644 index 0000000..0917642 --- /dev/null +++ b/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/database/dao/impl/MoneyDropDaoImpl.kt @@ -0,0 +1,56 @@ +package ru.astrainteractive.aspekt.module.moneydrop.database.dao.impl + +import kotlinx.coroutines.withContext +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.transaction +import ru.astrainteractive.aspekt.module.moneydrop.database.dao.MoneyDropDao +import ru.astrainteractive.aspekt.module.moneydrop.database.model.MoneyDropLocation +import ru.astrainteractive.aspekt.module.moneydrop.database.table.MoneyDropLocationTable +import ru.astrainteractive.astralibs.logging.JUtiltLogger +import ru.astrainteractive.astralibs.logging.Logger +import kotlin.coroutines.CoroutineContext + +internal class MoneyDropDaoImpl( + private val database: Database, + private val ioDispatcher: CoroutineContext +) : MoneyDropDao, Logger by JUtiltLogger("MoneyDropDao") { + override suspend fun addLocation(location: MoneyDropLocation) { + if (isLocationExists(location)) return + runCatching { + withContext(ioDispatcher) { + transaction(database) { + MoneyDropLocationTable.insert { + it[MoneyDropLocationTable.x] = location.x + it[MoneyDropLocationTable.y] = location.y + it[MoneyDropLocationTable.z] = location.z + it[MoneyDropLocationTable.world] = location.world + it[MoneyDropLocationTable.additionalConstraint] = location.additionalConstraint + } + } + } + }.onFailure { error { "#addLocation -> ${it.message}" } } + } + + override suspend fun isLocationExists(location: MoneyDropLocation): Boolean { + return runCatching { + withContext(ioDispatcher) { + transaction(database) { + MoneyDropLocationTable + .selectAll() + .where { + MoneyDropLocationTable.x.eq(location.x) + .and(MoneyDropLocationTable.y.eq(location.y)) + .and(MoneyDropLocationTable.z.eq(location.z)) + .and(MoneyDropLocationTable.world.eq(location.world)) + .and(MoneyDropLocationTable.additionalConstraint.eq(location.additionalConstraint)) + }.count() > 0 + } + } + } + .onFailure { error { "#isLocationExists -> ${it.message}" } } + .getOrElse { false } + } +} diff --git a/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/database/di/MoneyDropDaoModule.kt b/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/database/di/MoneyDropDaoModule.kt new file mode 100644 index 0000000..9c680c3 --- /dev/null +++ b/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/database/di/MoneyDropDaoModule.kt @@ -0,0 +1,45 @@ +package ru.astrainteractive.aspekt.module.moneydrop.database.di + +import kotlinx.coroutines.runBlocking +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger +import org.jetbrains.exposed.sql.addLogger +import org.jetbrains.exposed.sql.transactions.TransactionManager +import org.jetbrains.exposed.sql.transactions.transaction +import ru.astrainteractive.aspekt.module.moneydrop.database.dao.MoneyDropDao +import ru.astrainteractive.aspekt.module.moneydrop.database.dao.impl.MoneyDropDaoImpl +import ru.astrainteractive.aspekt.module.moneydrop.database.table.MoneyDropLocationTable +import java.io.File +import kotlin.coroutines.CoroutineContext + +internal interface MoneyDropDaoModule { + val dao: MoneyDropDao + + class Default( + dataFolder: File, + ioDispatcher: CoroutineContext + ) : MoneyDropDaoModule { + private val database by lazy { + val path = dataFolder.resolve("moneydrops.db").absolutePath + val database = Database.connect( + url = "jdbc:sqlite:$path", + driver = "org.sqlite.JDBC" + ) + TransactionManager.manager.defaultIsolationLevel = java.sql.Connection.TRANSACTION_SERIALIZABLE + runBlocking { + transaction(database) { + addLogger(Slf4jSqlDebugLogger) + SchemaUtils.create( + MoneyDropLocationTable, + ) + } + } + database + } + override val dao: MoneyDropDao = MoneyDropDaoImpl( + database = database, + ioDispatcher = ioDispatcher + ) + } +} diff --git a/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/database/model/MoneyDropLocation.kt b/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/database/model/MoneyDropLocation.kt new file mode 100644 index 0000000..aa089c3 --- /dev/null +++ b/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/database/model/MoneyDropLocation.kt @@ -0,0 +1,12 @@ +package ru.astrainteractive.aspekt.module.moneydrop.database.model + +import kotlinx.serialization.Serializable + +@Serializable +internal data class MoneyDropLocation( + val x: Int, + val y: Int, + val z: Int, + val world: String, + val additionalConstraint: String? = null +) diff --git a/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/database/table/MoneyDropLocationTable.kt b/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/database/table/MoneyDropLocationTable.kt new file mode 100644 index 0000000..886c334 --- /dev/null +++ b/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/database/table/MoneyDropLocationTable.kt @@ -0,0 +1,11 @@ +package ru.astrainteractive.aspekt.module.moneydrop.database.table + +import org.jetbrains.exposed.dao.id.LongIdTable + +internal object MoneyDropLocationTable : LongIdTable(name = "MONEY_DROP_LOCATION") { + val x = integer("x") + val y = integer("y") + val z = integer("z") + val world = text("world") + val additionalConstraint = text("additional_constraint").nullable() +} diff --git a/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/di/MoneyDropDependencies.kt b/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/di/MoneyDropDependencies.kt index c90dbd0..470c0d3 100644 --- a/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/di/MoneyDropDependencies.kt +++ b/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/di/MoneyDropDependencies.kt @@ -3,17 +3,15 @@ package ru.astrainteractive.aspekt.module.moneydrop.di import org.bukkit.plugin.java.JavaPlugin import ru.astrainteractive.aspekt.di.CoreModule import ru.astrainteractive.aspekt.module.moneydrop.MoneyDropController -import ru.astrainteractive.aspekt.plugin.PluginConfiguration import ru.astrainteractive.aspekt.plugin.PluginTranslation import ru.astrainteractive.astralibs.economy.EconomyProvider import ru.astrainteractive.astralibs.event.EventListener import ru.astrainteractive.astralibs.kyori.KyoriComponentSerializer import ru.astrainteractive.klibs.kdi.getValue -interface MoneyDropDependencies { +internal interface MoneyDropDependencies { val eventListener: EventListener val plugin: JavaPlugin - val configuration: PluginConfiguration val moneyDropController: MoneyDropController val economyProvider: EconomyProvider? val kyoriComponentSerializer: KyoriComponentSerializer @@ -25,7 +23,6 @@ interface MoneyDropDependencies { ) : MoneyDropDependencies { override val eventListener: EventListener = coreModule.eventListener override val plugin: JavaPlugin by coreModule.plugin - override val configuration: PluginConfiguration by coreModule.pluginConfig override val economyProvider: EconomyProvider? by coreModule.economyProvider override val kyoriComponentSerializer by coreModule.kyoriComponentSerializer override val translation: PluginTranslation by coreModule.translation diff --git a/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/di/MoneyDropModule.kt b/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/di/MoneyDropModule.kt index c396d3a..b1d9eda 100644 --- a/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/di/MoneyDropModule.kt +++ b/modules/moneydrop/src/main/kotlin/ru/astrainteractive/aspekt/module/moneydrop/di/MoneyDropModule.kt @@ -3,24 +3,33 @@ package ru.astrainteractive.aspekt.module.moneydrop.di import ru.astrainteractive.aspekt.di.CoreModule import ru.astrainteractive.aspekt.module.moneydrop.MoneyDropController import ru.astrainteractive.aspekt.module.moneydrop.MoneyDropEvent +import ru.astrainteractive.aspekt.module.moneydrop.database.di.MoneyDropDaoModule import ru.astrainteractive.astralibs.lifecycle.Lifecycle interface MoneyDropModule { val lifecycle: Lifecycle class Default(coreModule: CoreModule) : MoneyDropModule { + private val moneyDropDaoModule by lazy { + MoneyDropDaoModule.Default( + dataFolder = coreModule.plugin.value.dataFolder, + ioDispatcher = coreModule.dispatchers.IO + ) + } private val moneyDropController: MoneyDropController by lazy { MoneyDropController( pluginConfigurationDependency = coreModule.pluginConfig, kyoriComponentSerializerDependency = coreModule.kyoriComponentSerializer, - translationDependency = coreModule.translation + translationDependency = coreModule.translation, + dispatchers = coreModule.dispatchers, + dao = moneyDropDaoModule.dao ) } private val moneyDropEvent: MoneyDropEvent by lazy { MoneyDropEvent( dependencies = MoneyDropDependencies.Default( coreModule = coreModule, - moneyDropController = moneyDropController + moneyDropController = moneyDropController, ) ) } diff --git a/modules/moneydrop/src/test/kotlin/ru/astrainteractive/aspekt/module/moneydrop/database/dao/MoneyDropDaoTest.kt b/modules/moneydrop/src/test/kotlin/ru/astrainteractive/aspekt/module/moneydrop/database/dao/MoneyDropDaoTest.kt new file mode 100644 index 0000000..33c7f1e --- /dev/null +++ b/modules/moneydrop/src/test/kotlin/ru/astrainteractive/aspekt/module/moneydrop/database/dao/MoneyDropDaoTest.kt @@ -0,0 +1,48 @@ +package ru.astrainteractive.aspekt.module.moneydrop.database.dao + +import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.runTest +import ru.astrainteractive.aspekt.module.moneydrop.database.di.MoneyDropDaoModule +import ru.astrainteractive.aspekt.module.moneydrop.database.model.MoneyDropLocation +import java.io.File +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +internal class MoneyDropDaoTest { + private val scheduler = TestCoroutineScheduler() + private var _module: MoneyDropDaoModule? = null + private val requireModule: MoneyDropDaoModule + get() = _module ?: error("Module not set") + private val folder = File("./temp") + + @BeforeTest + fun setup() { + folder.mkdirs() + folder.deleteOnExit() + _module = MoneyDropDaoModule.Default( + dataFolder = folder, + ioDispatcher = scheduler + ) + } + + @AfterTest + fun tearDown() { + folder.deleteRecursively() + } + + @Test + fun testAll() = runTest(scheduler) { + val location = MoneyDropLocation( + x = 0, + y = 0, + z = 0, + world = "world" + ) + assertFalse(requireModule.dao.isLocationExists(location)) + requireModule.dao.addLocation(location) + assertTrue(requireModule.dao.isLocationExists(location)) + } +}