diff --git a/gradle.properties b/gradle.properties index e396d560..608037cf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ makeevrserg.java.ktarget=21 # Project makeevrserg.project.name=SoulKeeper makeevrserg.project.group=ru.astrainteractive.soulkeeper -makeevrserg.project.version.string=1.2.9 +makeevrserg.project.version.string=1.2.10 makeevrserg.project.description=Keep your items after death makeevrserg.project.developers=makeevrserg|Makeev Roman|makeevrserg@gmail.com makeevrserg.project.url=https://github.com/Astra-Interactive/SoulKeeper diff --git a/instances/bukkit/src/main/resources/plugin.yml b/instances/bukkit/src/main/resources/plugin.yml index 571d064e..b465ae48 100644 --- a/instances/bukkit/src/main/resources/plugin.yml +++ b/instances/bukkit/src/main/resources/plugin.yml @@ -16,4 +16,5 @@ libraries: - "com.h2database:h2:2.4.240" commands: souls: - skreload: \ No newline at end of file + skreload: + soulkrate: \ No newline at end of file diff --git a/modules/command-bukkit/build.gradle.kts b/modules/command-bukkit/build.gradle.kts index 62f45405..55e9f39f 100644 --- a/modules/command-bukkit/build.gradle.kts +++ b/modules/command-bukkit/build.gradle.kts @@ -5,6 +5,7 @@ plugins { dependencies { compileOnly(libs.kotlin.coroutines.core) + implementation(libs.kotlin.serialization.json) implementation(libs.minecraft.astralibs.core) implementation(libs.minecraft.astralibs.core.bukkit) diff --git a/modules/command-bukkit/src/main/kotlin/ru/astrainteractive/soulkeeper/command/di/CommandModule.kt b/modules/command-bukkit/src/main/kotlin/ru/astrainteractive/soulkeeper/command/di/CommandModule.kt index 54244e42..7df56f9b 100644 --- a/modules/command-bukkit/src/main/kotlin/ru/astrainteractive/soulkeeper/command/di/CommandModule.kt +++ b/modules/command-bukkit/src/main/kotlin/ru/astrainteractive/soulkeeper/command/di/CommandModule.kt @@ -3,6 +3,7 @@ package ru.astrainteractive.soulkeeper.command.di import ru.astrainteractive.astralibs.command.api.registrar.PaperCommandRegistrarContext import ru.astrainteractive.astralibs.lifecycle.Lifecycle import ru.astrainteractive.soulkeeper.command.reload.SoulsReloadCommandRegistrar +import ru.astrainteractive.soulkeeper.command.soulkrate.SoulKrateCommandRegistrar import ru.astrainteractive.soulkeeper.command.souls.SoulsCommandExecutor import ru.astrainteractive.soulkeeper.command.souls.SoulsListCommandRegistrar import ru.astrainteractive.soulkeeper.core.di.BukkitCoreModule @@ -30,6 +31,12 @@ class CommandModule( kyoriKrate = coreModule.kyoriComponentSerializer ) ).register() + SoulKrateCommandRegistrar( + registrarContext = paperCommandRegistrar, + stringFormat = coreModule.yamlFormat, + dataFolder = coreModule.dataFolder, + ioScope = coreModule.ioScope + ).register() SoulsReloadCommandRegistrar( plugin = bukkitCoreModule.plugin, translationKrate = coreModule.translation, diff --git a/modules/command-bukkit/src/main/kotlin/ru/astrainteractive/soulkeeper/command/soulkrate/SoulKrateCommandRegistrar.kt b/modules/command-bukkit/src/main/kotlin/ru/astrainteractive/soulkeeper/command/soulkrate/SoulKrateCommandRegistrar.kt new file mode 100644 index 00000000..8048d4c8 --- /dev/null +++ b/modules/command-bukkit/src/main/kotlin/ru/astrainteractive/soulkeeper/command/soulkrate/SoulKrateCommandRegistrar.kt @@ -0,0 +1,75 @@ +package ru.astrainteractive.soulkeeper.command.soulkrate + +import com.mojang.brigadier.arguments.IntegerArgumentType +import com.mojang.brigadier.arguments.LongArgumentType +import com.mojang.brigadier.arguments.StringArgumentType +import com.mojang.brigadier.tree.LiteralCommandNode +import io.papermc.paper.command.brigadier.CommandSourceStack +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.serialization.StringFormat +import ru.astrainteractive.astralibs.command.api.registrar.PaperCommandRegistrarContext +import ru.astrainteractive.astralibs.command.api.util.argument +import ru.astrainteractive.astralibs.command.api.util.command +import ru.astrainteractive.astralibs.command.api.util.requireArgument +import ru.astrainteractive.astralibs.command.api.util.requirePermission +import ru.astrainteractive.astralibs.command.api.util.requirePlayer +import ru.astrainteractive.astralibs.command.api.util.runs +import ru.astrainteractive.klibs.mikro.core.logging.JUtiltLogger +import ru.astrainteractive.klibs.mikro.core.logging.Logger +import ru.astrainteractive.soulkeeper.core.plugin.PluginPermission +import ru.astrainteractive.soulkeeper.core.serialization.ItemStackSerializer +import ru.astrainteractive.soulkeeper.core.serialization.ItemStackSerializer.decodeFromString +import ru.astrainteractive.soulkeeper.module.souls.database.model.StringFormatObject +import ru.astrainteractive.soulkeeper.module.souls.krate.PlayerSoulKrate +import java.io.File +import java.time.Instant +import java.util.UUID + +internal class SoulKrateCommandRegistrar( + private val registrarContext: PaperCommandRegistrarContext, + private val stringFormat: StringFormat, + private val dataFolder: File, + private val ioScope: CoroutineScope +) : Logger by JUtiltLogger("SoulKrateCommandRegistrar") { + private fun createNode(): LiteralCommandNode { + return command("soulkrate") { + argument("uuid", StringArgumentType.string()) { uuidArg -> + argument("instant", LongArgumentType.longArg()) { instantArg -> + argument("index", IntegerArgumentType.integer()) { indexArg -> + runs { ctx -> + ctx.requirePermission(PluginPermission.LoadSouls) + val player = ctx.requirePlayer() + val instant = ctx.requireArgument(instantArg).let(Instant::ofEpochSecond) + val index = ctx.requireArgument(indexArg) + val uuid = ctx.requireArgument(uuidArg).let(UUID::fromString) + ioScope.launch { + val soul = PlayerSoulKrate( + stringFormat = stringFormat, + dataFolder = dataFolder, + createdAt = instant, + ownerUUID = uuid, + readIndex = index + ).getValue() + val items = soul?.items + .orEmpty() + .map(StringFormatObject::raw) + .map(ItemStackSerializer::decodeFromString) + .mapNotNull { itemStackResult -> + itemStackResult + .onFailure { error(it) { "Failed to deserialize item stack" } } + .getOrNull() + } + player.inventory.addItem(*items.toTypedArray()) + } + } + } + } + } + }.build() + } + + fun register() { + registrarContext.registerWhenReady(createNode()) + } +} diff --git a/modules/core/src/main/kotlin/ru/astrainteractive/soulkeeper/core/plugin/PluginPermission.kt b/modules/core/src/main/kotlin/ru/astrainteractive/soulkeeper/core/plugin/PluginPermission.kt index 4d777464..e2fa1256 100644 --- a/modules/core/src/main/kotlin/ru/astrainteractive/soulkeeper/core/plugin/PluginPermission.kt +++ b/modules/core/src/main/kotlin/ru/astrainteractive/soulkeeper/core/plugin/PluginPermission.kt @@ -7,4 +7,5 @@ sealed class PluginPermission(override val value: String) : Permission { data object ViewAllSouls : PluginPermission("soulkeeper.all") data object FreeAllSouls : PluginPermission("soulkeeper.free.all") data object TeleportToSouls : PluginPermission("soulkeeper.teleport") + data object LoadSouls : PluginPermission("soulkeeper.load") } diff --git a/modules/dao/build.gradle.kts b/modules/dao/build.gradle.kts index df1058ad..233c759e 100644 --- a/modules/dao/build.gradle.kts +++ b/modules/dao/build.gradle.kts @@ -1,5 +1,6 @@ plugins { kotlin("jvm") + kotlin("plugin.serialization") } dependencies { @@ -8,8 +9,10 @@ dependencies { implementation(libs.exposed.dao) implementation(libs.exposed.jdbc) implementation(libs.exposed.java.time) + implementation(libs.kotlin.serialization.json) // AstraLibs implementation(libs.minecraft.astralibs.core) implementation(libs.klibs.mikro.extensions) implementation(libs.klibs.mikro.core) + implementation(libs.klibs.kstorage) } diff --git a/modules/dao/src/main/kotlin/ru/astrainteractive/soulkeeper/module/souls/database/model/DefaultSoul.kt b/modules/dao/src/main/kotlin/ru/astrainteractive/soulkeeper/module/souls/database/model/DefaultSoul.kt index 4723e4c9..6c5c7825 100644 --- a/modules/dao/src/main/kotlin/ru/astrainteractive/soulkeeper/module/souls/database/model/DefaultSoul.kt +++ b/modules/dao/src/main/kotlin/ru/astrainteractive/soulkeeper/module/souls/database/model/DefaultSoul.kt @@ -1,12 +1,18 @@ package ru.astrainteractive.soulkeeper.module.souls.database.model +import kotlinx.serialization.Serializable import ru.astrainteractive.astralibs.server.location.Location +import ru.astrainteractive.klibs.mikro.extensions.serialization.JInstantSerializer +import ru.astrainteractive.klibs.mikro.extensions.serialization.UUIDSerializer import java.time.Instant import java.util.UUID +@Serializable data class DefaultSoul( + @Serializable(with = UUIDSerializer::class) override val ownerUUID: UUID, override val ownerLastName: String, + @Serializable(with = JInstantSerializer::class) override val createdAt: Instant, override val isFree: Boolean, override val location: Location, diff --git a/modules/dao/src/main/kotlin/ru/astrainteractive/soulkeeper/module/souls/database/model/StringFormatObject.kt b/modules/dao/src/main/kotlin/ru/astrainteractive/soulkeeper/module/souls/database/model/StringFormatObject.kt index fa86accd..3f21a1dc 100644 --- a/modules/dao/src/main/kotlin/ru/astrainteractive/soulkeeper/module/souls/database/model/StringFormatObject.kt +++ b/modules/dao/src/main/kotlin/ru/astrainteractive/soulkeeper/module/souls/database/model/StringFormatObject.kt @@ -1,3 +1,6 @@ package ru.astrainteractive.soulkeeper.module.souls.database.model +import kotlinx.serialization.Serializable + +@Serializable data class StringFormatObject(val raw: String) diff --git a/modules/dao/src/main/kotlin/ru/astrainteractive/soulkeeper/module/souls/krate/PlayerSoulKrate.kt b/modules/dao/src/main/kotlin/ru/astrainteractive/soulkeeper/module/souls/krate/PlayerSoulKrate.kt new file mode 100644 index 00000000..9fcfc59d --- /dev/null +++ b/modules/dao/src/main/kotlin/ru/astrainteractive/soulkeeper/module/souls/krate/PlayerSoulKrate.kt @@ -0,0 +1,67 @@ +package ru.astrainteractive.soulkeeper.module.souls.krate + +import kotlinx.serialization.StringFormat +import ru.astrainteractive.astralibs.util.parse +import ru.astrainteractive.astralibs.util.writeIntoFile +import ru.astrainteractive.klibs.kstorage.suspend.SuspendMutableKrate +import ru.astrainteractive.klibs.kstorage.suspend.impl.DefaultSuspendMutableKrate +import ru.astrainteractive.klibs.mikro.core.logging.JUtiltLogger +import ru.astrainteractive.klibs.mikro.core.logging.Logger +import ru.astrainteractive.soulkeeper.module.souls.database.model.DefaultSoul +import java.io.File +import java.time.Instant +import java.util.UUID + +class PlayerSoulKrate( + stringFormat: StringFormat, + private val dataFolder: File, + private val createdAt: Instant, + private val ownerUUID: UUID, + private val readIndex: Int = 0 +) : SuspendMutableKrate, Logger by JUtiltLogger("SoulKeeper-PlayerSoulKrate") { + private fun createFile(): File { + val parentDir = dataFolder.resolve("$ownerUUID") + parentDir.mkdirs() + + var index = 0 + var file: File + do { + file = parentDir.resolve("${createdAt.epochSecond}_$index.yml") + index++ + } while (file.exists()) + file.createNewFile() + return file + } + + private val krate = DefaultSuspendMutableKrate( + factory = { null }, + loader = { + val file = dataFolder + .resolve("$ownerUUID") + .resolve("${createdAt.epochSecond}_$readIndex.yml") + stringFormat.parse(file) + .onFailure { error(it) { "Failed to load soul for $ownerUUID" } } + .getOrNull() + }, + saver = saver@{ defaultSoul -> + val file = createFile() + if (defaultSoul == null) { + file.renameTo(file.resolveSibling("${file.nameWithoutExtension}.deleted")) + return@saver + } + stringFormat.writeIntoFile(defaultSoul, file) + } + ) + + override suspend fun save(value: DefaultSoul?) { + krate.save(value) + } + + override suspend fun reset() { + krate.reset() + } + + override suspend fun getValue(): DefaultSoul? { + return krate.getValue() + } +} diff --git a/modules/event-bukkit/src/main/kotlin/ru/astrainteractive/soulkeeper/module/event/di/BukkitEventModule.kt b/modules/event-bukkit/src/main/kotlin/ru/astrainteractive/soulkeeper/module/event/di/BukkitEventModule.kt index 7c2f9cc6..2f7021d7 100644 --- a/modules/event-bukkit/src/main/kotlin/ru/astrainteractive/soulkeeper/module/event/di/BukkitEventModule.kt +++ b/modules/event-bukkit/src/main/kotlin/ru/astrainteractive/soulkeeper/module/event/di/BukkitEventModule.kt @@ -15,6 +15,9 @@ class BukkitEventModule( private val event = BukkitSoulEvents( soulsDao = soulsDaoModule.soulsDao, soulsConfigKrate = coreModule.soulsConfigKrate, + ioScope = coreModule.ioScope, + dataFolder = coreModule.dataFolder, + stringFormat = coreModule.yamlFormat ) val lifecycle = Lifecycle.Lambda( onEnable = { diff --git a/modules/event-bukkit/src/main/kotlin/ru/astrainteractive/soulkeeper/module/event/event/BukkitSoulEvents.kt b/modules/event-bukkit/src/main/kotlin/ru/astrainteractive/soulkeeper/module/event/event/BukkitSoulEvents.kt index 8fc99aad..b2bff988 100644 --- a/modules/event-bukkit/src/main/kotlin/ru/astrainteractive/soulkeeper/module/event/event/BukkitSoulEvents.kt +++ b/modules/event-bukkit/src/main/kotlin/ru/astrainteractive/soulkeeper/module/event/event/BukkitSoulEvents.kt @@ -1,16 +1,17 @@ package ru.astrainteractive.soulkeeper.module.event.event -import kotlinx.coroutines.cancel +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import kotlinx.serialization.StringFormat import org.bukkit.World +import org.bukkit.entity.Player import org.bukkit.event.EventHandler import org.bukkit.event.EventPriority import org.bukkit.event.entity.PlayerDeathEvent -import ru.astrainteractive.astralibs.coroutines.withTimings import ru.astrainteractive.astralibs.event.EventListener +import ru.astrainteractive.astralibs.server.location.Location import ru.astrainteractive.klibs.kstorage.api.CachedKrate import ru.astrainteractive.klibs.kstorage.util.getValue -import ru.astrainteractive.klibs.mikro.core.coroutines.CoroutineFeature import ru.astrainteractive.soulkeeper.core.plugin.SoulsConfig import ru.astrainteractive.soulkeeper.core.serialization.ItemStackSerializer import ru.astrainteractive.soulkeeper.core.util.playSoundForPlayer @@ -20,29 +21,48 @@ import ru.astrainteractive.soulkeeper.core.util.toDatabaseLocation import ru.astrainteractive.soulkeeper.module.souls.dao.SoulsDao import ru.astrainteractive.soulkeeper.module.souls.database.model.DefaultSoul import ru.astrainteractive.soulkeeper.module.souls.database.model.StringFormatObject +import ru.astrainteractive.soulkeeper.module.souls.krate.PlayerSoulKrate +import java.io.File import java.time.Instant import kotlin.time.Duration.Companion.seconds internal class BukkitSoulEvents( private val soulsDao: SoulsDao, - soulsConfigKrate: CachedKrate + private val ioScope: CoroutineScope, + private val dataFolder: File, + private val stringFormat: StringFormat, + soulsConfigKrate: CachedKrate, ) : EventListener { - private val scope = CoroutineFeature.IO.withTimings() private val soulsConfig by soulsConfigKrate - @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH) - fun playerDeathEvent(event: PlayerDeathEvent) { - val soulItems = when { + private fun getAndClearItems(event: PlayerDeathEvent): List { + return when { event.keepInventory -> emptyList() - else -> { - val drops = event.drops.toList() - event.drops.clear() - drops + event.drops + .map(ItemStackSerializer::encodeToString) + .map(::StringFormatObject) + .also { event.drops.clear() } } } + } + + private fun getSoulLocation(event: PlayerDeathEvent): Location { + return when { + event.player.location.world.environment == World.Environment.THE_END -> { + val endLocation = event.player.location.clone() + if (endLocation.y < soulsConfig.endLocationLimitY) { + endLocation.y = soulsConfig.endLocationLimitY + } + endLocation + } - val droppedXp = when { + else -> event.player.location + }.toDatabaseLocation() + } + + private fun getAndClearDroppedXp(event: PlayerDeathEvent): Int { + return when { event.keepLevel -> 0 else -> { @@ -51,42 +71,42 @@ internal class BukkitSoulEvents( exp } } + } - if (soulItems.isEmpty() && droppedXp <= 0) return + private fun playEffects(soul: DefaultSoul, player: Player) { + soul.location + .toBukkitLocation() + .spawnParticleForPlayer(player, soulsConfig.particles.soulCreated) + soul.location + .toBukkitLocation() + .playSoundForPlayer(player, soulsConfig.sounds.soulDropped) + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH) + fun playerDeathEvent(event: PlayerDeathEvent) { + val soulItems = getAndClearItems(event) + val soulExp = getAndClearDroppedXp(event) + + if (soulItems.isEmpty() && soulExp <= 0) return - val bukkitSoul = DefaultSoul( - exp = droppedXp, + val soul = DefaultSoul( + exp = soulExp, ownerUUID = event.player.uniqueId, ownerLastName = event.player.name, createdAt = Instant.now(), isFree = soulsConfig.soulFreeAfter == 0.seconds, - location = when { - event.player.location.world.environment == World.Environment.THE_END -> { - val endLocation = event.player.location.clone() - if (endLocation.y < soulsConfig.endLocationLimitY) { - endLocation.y = soulsConfig.endLocationLimitY - } - endLocation - } - - else -> event.player.location - }.toDatabaseLocation(), - items = soulItems - .map(ItemStackSerializer::encodeToString) - .map(::StringFormatObject), - ) - bukkitSoul.location.toBukkitLocation().spawnParticleForPlayer( - event.player, - soulsConfig.particles.soulCreated, + location = getSoulLocation(event), + items = soulItems, ) - bukkitSoul.location.toBukkitLocation().playSoundForPlayer(event.player, soulsConfig.sounds.soulDropped) - scope.launch { - soulsDao.insertSoul(bukkitSoul) + ioScope.launch { + PlayerSoulKrate( + stringFormat = stringFormat, + dataFolder = dataFolder, + createdAt = soul.createdAt, + ownerUUID = soul.ownerUUID, + ).save(soul) } - } - - override fun onDisable() { - scope.cancel() - super.onDisable() + playEffects(soul, event.player) + ioScope.launch { soulsDao.insertSoul(soul) } } } diff --git a/modules/event-neoforge/src/main/kotlin/ru/astrainteractive/soulkeeper/module/event/di/ForgeEventModule.kt b/modules/event-neoforge/src/main/kotlin/ru/astrainteractive/soulkeeper/module/event/di/ForgeEventModule.kt index 7c1ef381..0b270e1b 100644 --- a/modules/event-neoforge/src/main/kotlin/ru/astrainteractive/soulkeeper/module/event/di/ForgeEventModule.kt +++ b/modules/event-neoforge/src/main/kotlin/ru/astrainteractive/soulkeeper/module/event/di/ForgeEventModule.kt @@ -17,7 +17,10 @@ class ForgeEventModule( soulsConfigKrate = coreModule.soulsConfigKrate, effectEmitter = effectEmitter, mainScope = coreModule.mainScope, - dispatchers = coreModule.dispatchers + dispatchers = coreModule.dispatchers, + dataFolder = coreModule.dataFolder, + stringFormat = coreModule.yamlFormat, + ioScope = coreModule.ioScope ) val lifecycle = Lifecycle.Lambda( diff --git a/modules/event-neoforge/src/main/kotlin/ru/astrainteractive/soulkeeper/module/event/event/ForgeSoulEvents.kt b/modules/event-neoforge/src/main/kotlin/ru/astrainteractive/soulkeeper/module/event/event/ForgeSoulEvents.kt index f0242311..924e7f7d 100644 --- a/modules/event-neoforge/src/main/kotlin/ru/astrainteractive/soulkeeper/module/event/event/ForgeSoulEvents.kt +++ b/modules/event-neoforge/src/main/kotlin/ru/astrainteractive/soulkeeper/module/event/event/ForgeSoulEvents.kt @@ -4,13 +4,14 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext +import kotlinx.serialization.StringFormat import net.minecraft.resources.ResourceKey import net.minecraft.server.level.ServerPlayer import net.minecraft.world.entity.item.ItemEntity -import net.minecraft.world.item.ItemStack import net.minecraft.world.level.GameRules import net.minecraft.world.level.Level import net.neoforged.bus.api.EventPriority @@ -21,7 +22,6 @@ import ru.astrainteractive.astralibs.server.location.Location import ru.astrainteractive.astralibs.server.player.OnlineMinecraftPlayer import ru.astrainteractive.astralibs.server.util.asLocatable import ru.astrainteractive.astralibs.server.util.asOnlineMinecraftPlayer -import ru.astrainteractive.astralibs.server.util.toPlain import ru.astrainteractive.klibs.kstorage.api.CachedKrate import ru.astrainteractive.klibs.kstorage.util.getValue import ru.astrainteractive.klibs.mikro.core.dispatchers.KotlinDispatchers @@ -33,18 +33,24 @@ import ru.astrainteractive.soulkeeper.module.souls.dao.SoulsDao import ru.astrainteractive.soulkeeper.module.souls.database.model.DefaultSoul import ru.astrainteractive.soulkeeper.module.souls.database.model.ItemDatabaseSoul import ru.astrainteractive.soulkeeper.module.souls.database.model.StringFormatObject +import ru.astrainteractive.soulkeeper.module.souls.krate.PlayerSoulKrate import ru.astrainteractive.soulkeeper.module.souls.platform.EffectEmitter import ru.astrainteractive.soulkeeper.module.souls.platform.ItemStackSerializer +import java.io.File import java.time.Instant import kotlin.collections.isNotEmpty import kotlin.collections.map import kotlin.collections.orEmpty import kotlin.time.Duration.Companion.seconds +@Suppress("LongParameterList") internal class ForgeSoulEvents( private val soulsDao: SoulsDao, private val effectEmitter: EffectEmitter, private val dispatchers: KotlinDispatchers, + private val dataFolder: File, + private val stringFormat: StringFormat, + private val ioScope: CoroutineScope, mainScope: CoroutineScope, soulsConfigKrate: CachedKrate ) : Logger by JUtiltLogger("SoulKeeper-ForgeSoulEvents") { @@ -68,36 +74,50 @@ internal class ForgeSoulEvents( } } - private suspend fun updateSoul( + private suspend fun updateSoulUnsafe( soul: ItemDatabaseSoul, droppedXp: Int?, - soulItems: List? + soulItems: List? ) { val updatedItems = soulItems - ?.map(ItemStackSerializer::encodeToString) - ?.map(::StringFormatObject) .orEmpty() .plus(soul.items) - soulsDao.updateSoul( - soul = soul.copy( - exp = droppedXp ?: soul.exp, - items = updatedItems, - hasItems = updatedItems.isNotEmpty() - ) + val updatedSoul = soul.copy( + exp = droppedXp ?: soul.exp, + items = updatedItems, + hasItems = updatedItems.isNotEmpty() ) + soulsDao.updateSoul(soul = updatedSoul) + ioScope.launch { + val defaultSoul = DefaultSoul( + ownerUUID = updatedSoul.ownerUUID, + ownerLastName = updatedSoul.ownerLastName, + createdAt = updatedSoul.createdAt, + isFree = updatedSoul.isFree, + location = updatedSoul.location, + exp = updatedSoul.exp, + items = updatedSoul.items + ) + PlayerSoulKrate( + stringFormat = stringFormat, + dataFolder = dataFolder, + createdAt = soul.createdAt, + ownerUUID = soul.ownerUUID + ).save(defaultSoul) + } } - private suspend fun createSoul( - serverPlayer: ServerPlayer, + private suspend fun createSoulUnsafe( + serverPlayer: OnlineMinecraftPlayer, droppedXp: Int?, - soulItems: List?, + soulItems: List?, location: Location, dimension: ResourceKey ) { val soul = DefaultSoul( exp = droppedXp ?: 0, ownerUUID = serverPlayer.uuid, - ownerLastName = serverPlayer.name.toPlain(), + ownerLastName = serverPlayer.name, createdAt = Instant.now(), isFree = soulsConfig.soulFreeAfter == 0.seconds, location = when (dimension) { @@ -107,19 +127,24 @@ internal class ForgeSoulEvents( else -> location }, - items = soulItems - .orEmpty() - .map(ItemStackSerializer::encodeToString) - .map(::StringFormatObject), + items = soulItems.orEmpty(), ) soulsDao.insertSoul(soul) + ioScope.launch { + PlayerSoulKrate( + stringFormat = stringFormat, + dataFolder = dataFolder, + createdAt = soul.createdAt, + ownerUUID = soul.ownerUUID + ).save(soul) + } } @Suppress("LongMethod") private suspend fun createOrUpdateSoul( serverPlayer: ServerPlayer, droppedXp: Int?, - soulItems: List? + soulItems: List? ) { mutex.withLock { val location = serverPlayer @@ -134,15 +159,15 @@ internal class ForgeSoulEvents( ?.getOrNull() if (existingSoul != null) { - updateSoul( + updateSoulUnsafe( soul = existingSoul, droppedXp = droppedXp, soulItems = soulItems ) } else { - createSoul( + createSoulUnsafe( location = location, - serverPlayer = serverPlayer, + serverPlayer = serverPlayer.asOnlineMinecraftPlayer(), droppedXp = droppedXp, soulItems = soulItems, dimension = serverPlayer.level().dimension() @@ -155,23 +180,43 @@ internal class ForgeSoulEvents( } } + private fun getAndClearDroppedXp(event: LivingExperienceDropEvent): Int { + val keepLevel = event.entity.level().gameRules.getBoolean(GameRules.RULE_KEEPINVENTORY) + return when { + keepLevel -> 0 + + else -> { + event.originalExperience + .times(soulsConfig.retainedXp) + .toInt() + .also { event.droppedExperience = 0 } + } + } + } + + private fun getAndClearItems(event: LivingDropsEvent): List { + val keepInventory = event.entity.level() + .gameRules + .getBoolean(GameRules.RULE_KEEPINVENTORY) + return when { + keepInventory -> emptyList() + else -> { + event.drops + .map(ItemEntity::getItem) + .map(ItemStackSerializer::encodeToString) + .map(::StringFormatObject) + .also { event.drops.clear() } + } + } + } + val expDropEvent = flowEvent(EventPriority.HIGHEST) .filter { !it.isCanceled } .onEach { event -> val serverPlayer = event.entity.tryCast() ?: return@onEach - val keepLevel = event.entity.level().gameRules.getBoolean(GameRules.RULE_KEEPINVENTORY) - - val droppedXp = when { - keepLevel -> 0 - else -> { - event.originalExperience - .times(soulsConfig.retainedXp) - .toInt() - } - } + val droppedXp = getAndClearDroppedXp(event) if (droppedXp <= 0) return@onEach - event.droppedExperience = 0 createOrUpdateSoul( serverPlayer = serverPlayer, @@ -184,18 +229,10 @@ internal class ForgeSoulEvents( val livingDropsEvent = flowEvent(EventPriority.HIGHEST) .filter { event -> !event.isCanceled } .onEach { event -> - info { "#livingDropsEvent ${event.drops.size} ${event.drops}" } val serverPlayer = event.entity.tryCast() ?: return@onEach - val keepInventory = event.entity.level().gameRules.getBoolean(GameRules.RULE_KEEPINVENTORY) - - val soulItems = when { - keepInventory -> emptyList() - else -> event.drops.map(ItemEntity::getItem) - } + val soulItems = getAndClearItems(event) if (soulItems.isEmpty()) return@onEach - event.drops.clear() - createOrUpdateSoul( serverPlayer = serverPlayer, droppedXp = null, diff --git a/modules/service-bukkit/src/main/kotlin/ru/astrainteractive/soulkeeper/module/souls/domain/BukkitPickUpItemsUseCase.kt b/modules/service-bukkit/src/main/kotlin/ru/astrainteractive/soulkeeper/module/souls/domain/BukkitPickUpItemsUseCase.kt index fec67329..c47db468 100644 --- a/modules/service-bukkit/src/main/kotlin/ru/astrainteractive/soulkeeper/module/souls/domain/BukkitPickUpItemsUseCase.kt +++ b/modules/service-bukkit/src/main/kotlin/ru/astrainteractive/soulkeeper/module/souls/domain/BukkitPickUpItemsUseCase.kt @@ -23,13 +23,24 @@ internal class BukkitPickUpItemsUseCase( Logger by JUtiltLogger("SoulKeeper-PickUpItemsUseCase") { override suspend fun invoke(player: OnlineMinecraftPlayer, soul: ItemDatabaseSoul): Output { - if (soul.items.isEmpty()) return Output.NoItemsPresent - val bukkitPlayer = Bukkit.getPlayer(player.uuid) ?: return Output.SomeItemsRemain + if (soul.items.isEmpty()) { + info { "Soul ${soul.id} has no items to collect" } + return Output.NoItemsPresent + } + val bukkitPlayer = Bukkit.getPlayer(player.uuid) + if (bukkitPlayer == null) { + info { "Player ${player.uuid} is not online" } + return Output.SomeItemsRemain + } val items = soul.items .map(StringFormatObject::raw) .map(ItemStackSerializer::decodeFromString) - .mapNotNull { itemStackResult -> itemStackResult.getOrNull() } + .mapNotNull { itemStackResult -> + itemStackResult + .onFailure { error(it) { "Failed to deserialize item stack" } } + .getOrNull() + } val notAddedItems = withContext(dispatchers.Main) { bukkitPlayer.inventory .addItem(*items.toTypedArray()) diff --git a/modules/service/src/main/kotlin/ru/astrainteractive/soulkeeper/module/souls/domain/GetNearestSoulUseCase.kt b/modules/service/src/main/kotlin/ru/astrainteractive/soulkeeper/module/souls/domain/GetNearestSoulUseCase.kt index b69df766..fb9f9cab 100644 --- a/modules/service/src/main/kotlin/ru/astrainteractive/soulkeeper/module/souls/domain/GetNearestSoulUseCase.kt +++ b/modules/service/src/main/kotlin/ru/astrainteractive/soulkeeper/module/souls/domain/GetNearestSoulUseCase.kt @@ -19,6 +19,6 @@ internal class GetNearestSoulUseCase( return soulsDao.getSoulsNear(player.asLocatable().getLocation(), 2) .getOrNull() .orEmpty() - .firstOrNull { it.isFree || it.ownerUUID == player.uuid } + .firstOrNull { soul -> soul.isFree || soul.ownerUUID == player.uuid } } }