diff --git a/build.gradle.kts b/build.gradle.kts index 0d06527..cd89a12 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -85,6 +85,10 @@ subprojects { implementation("com.charleskorn.kaml:kaml:0.38.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1") + // Persistence + implementation("org.jetbrains.xodus:xodus-openAPI:1.3.232") + implementation("org.jetbrains.xodus:xodus-entity-store:1.3.232") + // Paper API compileOnly("io.papermc.paper:paper-api:1.18.1-R0.1-SNAPSHOT") } diff --git a/foundation-core/src/main/kotlin/cloud/kubelet/foundation/core/FoundationCorePlugin.kt b/foundation-core/src/main/kotlin/cloud/kubelet/foundation/core/FoundationCorePlugin.kt index e19b771..84503f6 100644 --- a/foundation-core/src/main/kotlin/cloud/kubelet/foundation/core/FoundationCorePlugin.kt +++ b/foundation-core/src/main/kotlin/cloud/kubelet/foundation/core/FoundationCorePlugin.kt @@ -1,17 +1,22 @@ package cloud.kubelet.foundation.core -import cloud.kubelet.foundation.core.command.BackupCommand -import cloud.kubelet.foundation.core.command.GamemodeCommand -import cloud.kubelet.foundation.core.command.LeaderboardCommand -import cloud.kubelet.foundation.core.command.UpdateCommand +import cloud.kubelet.foundation.core.command.* +import cloud.kubelet.foundation.core.persist.PersistentStore +import cloud.kubelet.foundation.core.persist.setAllProperties +import io.papermc.paper.event.player.AsyncChatEvent import net.kyori.adventure.text.Component +import net.kyori.adventure.text.TextComponent import org.bukkit.GameMode import org.bukkit.command.CommandExecutor +import org.bukkit.event.EventHandler import org.bukkit.event.Listener import org.bukkit.plugin.java.JavaPlugin import java.nio.file.Path +import java.time.Instant +import java.util.concurrent.ConcurrentHashMap class FoundationCorePlugin : JavaPlugin(), Listener { + internal val persistentStores = ConcurrentHashMap() private lateinit var _pluginDataPath: Path var pluginDataPath: Path @@ -29,6 +34,13 @@ class FoundationCorePlugin : JavaPlugin(), Listener { _pluginDataPath = value } + /** + * Fetch a persistent store by name. Make sure the name is path-safe, descriptive and consistent across server runs. + */ + fun getPersistentStore(name: String) = persistentStores.getOrPut(name) { PersistentStore(this, name) } + + private lateinit var chatLogStore: PersistentStore + override fun onEnable() { pluginDataPath = dataFolder.toPath() val backupPath = pluginDataPath.resolve(BACKUPS_DIRECTORY) @@ -48,10 +60,12 @@ class FoundationCorePlugin : JavaPlugin(), Listener { registerCommandExecutor(listOf("adventure", "a"), GamemodeCommand(GameMode.ADVENTURE)) registerCommandExecutor(listOf("spectator", "sp"), GamemodeCommand(GameMode.SPECTATOR)) registerCommandExecutor(listOf("leaderboard", "lb"), LeaderboardCommand()) + registerCommandExecutor(listOf("pstorestats"), StoreStatsCommand(this)) val log = slF4JLogger log.info("Features:") Util.printFeatureStatus(log, "Backup", BACKUP_ENABLED) + chatLogStore = getPersistentStore("chat-logs") } private fun registerCommandExecutor(name: String, executor: CommandExecutor) { @@ -81,6 +95,26 @@ class FoundationCorePlugin : JavaPlugin(), Listener { server.sendMessage(component) }*/ + @EventHandler + private fun logOnChatMessage(e: AsyncChatEvent) { + val player = e.player + val message = e.message() + + if (message !is TextComponent) { + return + } + + val content = message.content() + chatLogStore.create("ChatMessageEvent") { + setAllProperties( + "timestamp" to Instant.now().toEpochMilli(), + "player.id" to player.identity().uuid().toString(), + "player.name" to player.name, + "message.content" to content + ) + } + } + companion object { private const val BACKUPS_DIRECTORY = "backups" diff --git a/foundation-core/src/main/kotlin/cloud/kubelet/foundation/core/command/StoreStatsCommand.kt b/foundation-core/src/main/kotlin/cloud/kubelet/foundation/core/command/StoreStatsCommand.kt new file mode 100644 index 0000000..9c33259 --- /dev/null +++ b/foundation-core/src/main/kotlin/cloud/kubelet/foundation/core/command/StoreStatsCommand.kt @@ -0,0 +1,19 @@ +package cloud.kubelet.foundation.core.command + +import cloud.kubelet.foundation.core.FoundationCorePlugin +import org.bukkit.command.Command +import org.bukkit.command.CommandExecutor +import org.bukkit.command.CommandSender + +class StoreStatsCommand(private val plugin: FoundationCorePlugin) : CommandExecutor { + override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + plugin.persistentStores.forEach { (name, store) -> + store.transact { tx -> + val types = tx.entityTypes + val counts = types.associateWith { type -> tx.getAll(type).size() }.toSortedMap() + sender.sendMessage("Store $name ->", *counts.map { " ${it.key} -> ${it.value} entries" }.toTypedArray()) + } + } + return true + } +} diff --git a/foundation-core/src/main/kotlin/cloud/kubelet/foundation/core/persist/PersistentStore.kt b/foundation-core/src/main/kotlin/cloud/kubelet/foundation/core/persist/PersistentStore.kt new file mode 100644 index 0000000..50cb276 --- /dev/null +++ b/foundation-core/src/main/kotlin/cloud/kubelet/foundation/core/persist/PersistentStore.kt @@ -0,0 +1,25 @@ +package cloud.kubelet.foundation.core.persist + +import cloud.kubelet.foundation.core.FoundationCorePlugin +import jetbrains.exodus.entitystore.Entity +import jetbrains.exodus.entitystore.PersistentEntityStores +import jetbrains.exodus.entitystore.StoreTransaction + +class PersistentStore(corePlugin: FoundationCorePlugin, fileStoreName: String) : AutoCloseable { + private val fileStorePath = corePlugin.pluginDataPath.resolve("persistence/${fileStoreName}") + internal val entityStore = PersistentEntityStores.newInstance(fileStorePath.toFile()) + + fun transact(block: (StoreTransaction) -> Unit) = entityStore.executeInTransaction(block) + + fun create(entityTypeName: String, populate: Entity.() -> Unit) = transact { tx -> + val entity = tx.newEntity(entityTypeName) + populate(entity) + } + + fun find(entityTypeName: String, propertyName: String, value: Comparable) = + transact { tx -> tx.find(entityTypeName, propertyName, value) } + + override fun close() { + entityStore.close() + } +} diff --git a/foundation-core/src/main/kotlin/cloud/kubelet/foundation/core/persist/XodusExtensions.kt b/foundation-core/src/main/kotlin/cloud/kubelet/foundation/core/persist/XodusExtensions.kt new file mode 100644 index 0000000..2852d95 --- /dev/null +++ b/foundation-core/src/main/kotlin/cloud/kubelet/foundation/core/persist/XodusExtensions.kt @@ -0,0 +1,7 @@ +package cloud.kubelet.foundation.core.persist + +import jetbrains.exodus.entitystore.Entity + +fun > Entity.setAllProperties(vararg entries: Pair) = entries.forEach { entry -> + setProperty(entry.first, entry.second) +} diff --git a/foundation-core/src/main/resources/plugin.yml b/foundation-core/src/main/resources/plugin.yml index 5e21ddd..ad20efc 100644 --- a/foundation-core/src/main/resources/plugin.yml +++ b/foundation-core/src/main/resources/plugin.yml @@ -45,3 +45,7 @@ commands: aliases: - lb permission: foundation.command.leaderboard + pstorestats: + description: Persistent Store Stats + usage: /pstorestats + permission: foundation.command.pstorestats