Major refactoring to use Koin.

This commit is contained in:
Logan Gorence 2021-12-23 22:44:02 +00:00
parent f8178c2307
commit 13479b1ae3
No known key found for this signature in database
GPG Key ID: 9743CEF10935949A
19 changed files with 269 additions and 143 deletions

View File

@ -82,6 +82,9 @@ subprojects {
implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("io.insert-koin:koin-core:3.1.4")
testImplementation("io.insert-koin:koin-test:3.1.4")
// Serialization
implementation("com.charleskorn.kaml:kaml:0.38.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1")

View File

@ -1,3 +1,5 @@
dependencies {
// TODO: might be able to ship all dependencies in core? are we duplicating classes in JARs?
implementation("software.amazon.awssdk:s3:2.17.102")
}

View File

@ -1,27 +1,27 @@
package cloud.kubelet.foundation.core
import cloud.kubelet.foundation.core.command.*
import cloud.kubelet.foundation.core.devupdate.DevUpdateServer
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.command.TabCompleter
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.plugin.java.JavaPlugin
import cloud.kubelet.foundation.core.abstraction.Feature
import cloud.kubelet.foundation.core.abstraction.FoundationPlugin
import cloud.kubelet.foundation.core.features.backup.BackupFeature
import cloud.kubelet.foundation.core.features.dev.DevFeature
import cloud.kubelet.foundation.core.features.player.PlayerFeature
import cloud.kubelet.foundation.core.features.stats.StatsFeature
import cloud.kubelet.foundation.core.features.update.UpdateFeature
import cloud.kubelet.foundation.core.features.world.WorldFeature
import org.koin.dsl.module
import java.nio.file.Path
import java.time.Instant
import java.util.concurrent.ConcurrentHashMap
class FoundationCorePlugin : JavaPlugin(), Listener {
internal val persistentStores = ConcurrentHashMap<String, PersistentStore>()
class FoundationCorePlugin : FoundationPlugin() {
private lateinit var _pluginDataPath: Path
private lateinit var chatLogStore: PersistentStore
private lateinit var devUpdateServer: DevUpdateServer
override val features: List<Feature>
get() = listOf(
BackupFeature(),
DevFeature(),
PlayerFeature(),
StatsFeature(),
UpdateFeature(),
WorldFeature(),
)
var pluginDataPath: Path
/**
@ -38,104 +38,15 @@ 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) }
override fun module() = module {
single { this@FoundationCorePlugin }
}
override fun onEnable() {
// Create core plugin directory.
pluginDataPath = dataFolder.toPath()
val backupPath = pluginDataPath.resolve(BACKUPS_DIRECTORY)
// Create Foundation plugin directories.
pluginDataPath.toFile().mkdir()
backupPath.toFile().mkdir()
// Register this as an event listener.
server.pluginManager.registerEvents(this, this)
// Register commands.
registerCommandExecutor("fbackup", BackupCommand(this, backupPath))
registerCommandExecutor("fupdate", UpdateCommand())
registerCommandExecutor(listOf("survival", "s"), GamemodeCommand(GameMode.SURVIVAL))
registerCommandExecutor(listOf("creative", "c"), GamemodeCommand(GameMode.CREATIVE))
registerCommandExecutor(listOf("adventure", "a"), GamemodeCommand(GameMode.ADVENTURE))
registerCommandExecutor(listOf("spectator", "sp"), GamemodeCommand(GameMode.SPECTATOR))
registerCommandExecutor(listOf("leaderboard", "lb"), LeaderboardCommand())
registerCommandExecutor("pstore", PersistentStoreCommand(this))
registerCommandExecutor("setspawn", SetSpawnCommand())
registerCommandExecutor("spawn", SpawnCommand())
val log = slF4JLogger
log.info("Features:")
Util.printFeatureStatus(log, "Backup", BACKUP_ENABLED)
chatLogStore = getPersistentStore("chat-logs")
devUpdateServer = DevUpdateServer(this)
devUpdateServer.enable()
}
override fun onDisable() {
persistentStores.values.forEach { store -> store.close() }
persistentStores.clear()
devUpdateServer.disable()
}
private fun registerCommandExecutor(name: String, executor: CommandExecutor) {
registerCommandExecutor(listOf(name), executor)
}
private fun registerCommandExecutor(names: List<String>, executor: CommandExecutor) {
for (name in names) {
val command = getCommand(name) ?: throw Exception("Failed to get $name command")
command.setExecutor(executor)
if (executor is TabCompleter) {
command.tabCompleter = executor
}
}
}
// TODO: Disabling chat reformatting until I do something with it and figure out how to make it
// be less disruptive.
/*@EventHandler
private fun onChatMessage(e: ChatEvent) {
return
e.isCancelled = true
val name = e.player.displayName()
val component = Component.empty()
.append(leftBracket)
.append(name)
.append(rightBracket)
.append(Component.text(' '))
.append(e.message())
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"
private val leftBracket: Component = Component.text('[')
private val rightBracket: Component = Component.text(']')
const val BACKUP_ENABLED = true
super.onEnable()
}
}

View File

@ -11,10 +11,6 @@ object Util {
private val whitespace: Component = Component.text(' ')
private val foundationName: Component = Component.text("Foundation")
fun printFeatureStatus(logger: Logger, feature: String?, state: Boolean) {
logger.info("{}: {}", feature, if (state) "Enabled" else "Disabled")
}
fun formatSystemMessage(message: String): Component {
return formatSystemMessage(TextColors.AMARANTH_PINK, message)
}

View File

@ -0,0 +1,31 @@
package cloud.kubelet.foundation.core.abstraction
import cloud.kubelet.foundation.core.FoundationCorePlugin
import org.bukkit.command.CommandExecutor
import org.bukkit.command.TabCompleter
import org.bukkit.event.Listener
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.dsl.module
abstract class Feature : KoinComponent, Listener {
private val plugin by inject<FoundationCorePlugin>()
open fun enable() {}
open fun disable() {}
open fun module() = module {}
protected fun registerCommandExecutor(name: String, executor: CommandExecutor) {
registerCommandExecutor(listOf(name), executor)
}
protected fun registerCommandExecutor(names: List<String>, executor: CommandExecutor) {
for (name in names) {
val command = plugin.getCommand(name) ?: throw Exception("Failed to get $name command")
command.setExecutor(executor)
if (executor is TabCompleter) {
command.tabCompleter = executor
}
}
}
}

View File

@ -0,0 +1,54 @@
package cloud.kubelet.foundation.core.abstraction
import org.bukkit.plugin.java.JavaPlugin
import org.koin.core.KoinApplication
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.core.module.Module
import org.koin.dsl.module
abstract class FoundationPlugin : JavaPlugin() {
private lateinit var pluginModule: Module
private lateinit var pluginApplication: KoinApplication
protected abstract val features: List<Feature>
override fun onEnable() {
pluginModule = module {
single { this@FoundationPlugin }
single { server }
single { config }
single { slF4JLogger }
}
// TODO: If we have another plugin using this class, we may need to use context isolation.
// https://insert-koin.io/docs/reference/koin-core/context-isolation
pluginApplication = startKoin {
modules(pluginModule)
modules(module())
}
features.forEach {
pluginApplication.modules(it.module())
}
features.forEach {
try {
slF4JLogger.info("Enabling feature: ${it.javaClass.simpleName}")
it.enable()
server.pluginManager.registerEvents(it, this)
} catch (e: Exception) {
slF4JLogger.error("Failed to enable feature: ${it.javaClass.simpleName}", e)
}
}
}
override fun onDisable() {
features.forEach {
it.disable()
}
stopKoin()
}
protected open fun module() = module {}
}

View File

@ -1,4 +1,4 @@
package cloud.kubelet.foundation.core.command
package cloud.kubelet.foundation.core.features.backup
import cloud.kubelet.foundation.core.FoundationCorePlugin
import cloud.kubelet.foundation.core.Util
@ -26,15 +26,6 @@ class BackupCommand(
override fun onCommand(
sender: CommandSender, command: Command, label: String, args: Array<String>
): Boolean {
if (!FoundationCorePlugin.BACKUP_ENABLED) {
sender.sendMessage(
Component
.text("Backup is not enabled.")
.color(TextColor.fromHexString("#FF0000"))
)
return true
}
if (RUNNING.get()) {
sender.sendMessage(
Component

View File

@ -0,0 +1,21 @@
package cloud.kubelet.foundation.core.features.backup
import cloud.kubelet.foundation.core.FoundationCorePlugin
import cloud.kubelet.foundation.core.abstraction.Feature
import org.koin.core.component.inject
class BackupFeature : Feature() {
private val plugin by inject<FoundationCorePlugin>()
override fun enable() {
// Create backup directory.
val backupPath = plugin.pluginDataPath.resolve(BACKUPS_DIRECTORY)
backupPath.toFile().mkdir()
registerCommandExecutor("fbackup", BackupCommand(plugin, backupPath))
}
companion object {
private const val BACKUPS_DIRECTORY = "backups"
}
}

View File

@ -0,0 +1,20 @@
package cloud.kubelet.foundation.core.features.dev
import cloud.kubelet.foundation.core.FoundationCorePlugin
import cloud.kubelet.foundation.core.abstraction.Feature
import cloud.kubelet.foundation.core.devupdate.DevUpdateServer
import org.koin.core.component.inject
class DevFeature : Feature() {
private val plugin = inject<FoundationCorePlugin>()
private lateinit var devUpdateServer: DevUpdateServer
override fun enable() {
devUpdateServer = DevUpdateServer(plugin.value)
devUpdateServer.enable()
}
override fun disable() {
devUpdateServer.disable()
}
}

View File

@ -0,0 +1,14 @@
package cloud.kubelet.foundation.core.features.player
import cloud.kubelet.foundation.core.abstraction.Feature
import cloud.kubelet.foundation.core.command.GamemodeCommand
import org.bukkit.GameMode
class PlayerFeature : Feature() {
override fun enable() {
registerCommandExecutor(listOf("survival", "s"), GamemodeCommand(GameMode.SURVIVAL))
registerCommandExecutor(listOf("creative", "c"), GamemodeCommand(GameMode.CREATIVE))
registerCommandExecutor(listOf("adventure", "a"), GamemodeCommand(GameMode.ADVENTURE))
registerCommandExecutor(listOf("spectator", "sp"), GamemodeCommand(GameMode.SPECTATOR))
}
}

View File

@ -1,4 +1,4 @@
package cloud.kubelet.foundation.core.command
package cloud.kubelet.foundation.core.features.stats
import cloud.kubelet.foundation.core.SortOrder
import cloud.kubelet.foundation.core.allPlayerStatisticsOf

View File

@ -1,15 +1,21 @@
package cloud.kubelet.foundation.core.command
package cloud.kubelet.foundation.core.features.stats
import cloud.kubelet.foundation.core.FoundationCorePlugin
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
import org.bukkit.command.TabCompleter
class PersistentStoreCommand(private val plugin: FoundationCorePlugin) : CommandExecutor, TabCompleter {
class PersistentStoreCommand(
private val statsFeature: StatsFeature
) : CommandExecutor, TabCompleter {
private val allSubCommands = mutableListOf("stats", "sample", "delete-all-entities")
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>): Boolean {
override fun onCommand(
sender: CommandSender,
command: Command,
label: String,
args: Array<out String>
): Boolean {
if (args.isEmpty()) {
sender.sendMessage("Invalid Command Usage.")
return true
@ -17,7 +23,7 @@ class PersistentStoreCommand(private val plugin: FoundationCorePlugin) : Command
when (args[0]) {
"stats" -> {
plugin.persistentStores.forEach { (name, store) ->
statsFeature.persistentStores.forEach { (name, store) ->
val counts = store.transact {
entityTypes.associateWith { type -> getAll(type).size() }.toSortedMap()
}
@ -36,7 +42,7 @@ class PersistentStoreCommand(private val plugin: FoundationCorePlugin) : Command
val storeName = args[1]
val entityTypeName = args[2]
val store = plugin.getPersistentStore(storeName)
val store = statsFeature.getPersistentStore(storeName)
store.transact {
val entities = getAll(entityTypeName).take(3)
for (entity in entities) {
@ -55,7 +61,7 @@ class PersistentStoreCommand(private val plugin: FoundationCorePlugin) : Command
val storeName = args[1]
val entityTypeName = args[2]
val store = plugin.getPersistentStore(storeName)
val store = statsFeature.getPersistentStore(storeName)
store.transact {
store.deleteAllEntities(entityTypeName)
}

View File

@ -0,0 +1,56 @@
package cloud.kubelet.foundation.core.features.stats
import cloud.kubelet.foundation.core.FoundationCorePlugin
import cloud.kubelet.foundation.core.abstraction.Feature
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.TextComponent
import org.bukkit.event.EventHandler
import org.koin.core.component.inject
import java.time.Instant
import java.util.concurrent.ConcurrentHashMap
class StatsFeature : Feature() {
private val plugin = inject<FoundationCorePlugin>()
private lateinit var chatLogStore: PersistentStore
// TODO: Move persistence stuff to its own module.
internal val persistentStores = ConcurrentHashMap<String, PersistentStore>()
override fun enable() {
chatLogStore = getPersistentStore("chat-logs")
registerCommandExecutor("pstore", PersistentStoreCommand(this))
}
override fun disable() {
persistentStores.values.forEach { store -> store.close() }
persistentStores.clear()
}
/**
* 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(plugin.value, name) }
@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
)
}
}
}

View File

@ -1,4 +1,4 @@
package cloud.kubelet.foundation.core.update
package cloud.kubelet.foundation.core.features.update
import kotlinx.serialization.Serializable

View File

@ -1,6 +1,5 @@
package cloud.kubelet.foundation.core.command
package cloud.kubelet.foundation.core.features.update
import cloud.kubelet.foundation.core.service.UpdateService
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
@ -15,4 +14,4 @@ class UpdateCommand : CommandExecutor {
UpdateService.updatePlugins(sender)
return true
}
}
}

View File

@ -0,0 +1,9 @@
package cloud.kubelet.foundation.core.features.update
import cloud.kubelet.foundation.core.abstraction.Feature
class UpdateFeature : Feature() {
override fun enable() {
registerCommandExecutor("fupdate", UpdateCommand())
}
}

View File

@ -1,11 +1,10 @@
package cloud.kubelet.foundation.core.service
package cloud.kubelet.foundation.core.features.update
import cloud.kubelet.foundation.core.update.UpdateUtil
import org.bukkit.command.CommandSender
import kotlin.io.path.name
import kotlin.io.path.toPath
// TODO: Switch to classes and use dependency injection with koin.
// TODO: Switch to a class and use dependency injection with koin.
object UpdateService {
fun updatePlugins(sender: CommandSender, onFinish: (() -> Unit)? = null) {
val updateDir = sender.server.pluginsFolder.resolve("update")

View File

@ -1,4 +1,4 @@
package cloud.kubelet.foundation.core.update
package cloud.kubelet.foundation.core.features.update
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.builtins.MapSerializer

View File

@ -0,0 +1,14 @@
package cloud.kubelet.foundation.core.features.world
import cloud.kubelet.foundation.core.abstraction.Feature
import cloud.kubelet.foundation.core.command.GamemodeCommand
import cloud.kubelet.foundation.core.command.SetSpawnCommand
import cloud.kubelet.foundation.core.command.SpawnCommand
import org.bukkit.GameMode
class WorldFeature : Feature() {
override fun enable() {
registerCommandExecutor("setspawn", SetSpawnCommand())
registerCommandExecutor("spawn", SpawnCommand())
}
}