Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -69,29 +69,29 @@ dependencies {
implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:$mccoroutineVersion")

// Minecraft server framework
compileOnly("io.papermc.paper:paper-api:$paperVersion")
"io.papermc.paper:paper-api:$paperVersion".let {
compileOnly(it)
testImplementation(it)
}

// Scoreboard framework
implementation("fr.mrmicky:fastboard:$fastboardVersion")

api("com.github.Rushyverse:core:6ae31a9250")

// Tests
testImplementation("com.github.seeseemelk:MockBukkit-v1.20:$mockBukkitVersion")
testImplementation(kotlin("test-junit5"))
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutineVersion")
testImplementation("io.kotest:kotest-assertions-core:$kotestVersion")
implementation("io.kotest:kotest-assertions-json:$kotestVersion")
testImplementation("io.kotest:kotest-assertions-json:$kotestVersion")

testImplementation("io.papermc.paper:paper-api:$paperVersion")
implementation("com.github.seeseemelk:MockBukkit-v1.20:$mockBukkitVersion")
testImplementation("org.junit.jupiter:junit-jupiter-params:$junitVersion")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$junitVersion")
testImplementation("io.insert-koin:koin-test:$koinVersion") {
exclude("org.jetbrains.kotlin", "kotlin-test-junit")
}
testImplementation("io.mockk:mockk:$mockkVersion")
testImplementation("org.slf4j:slf4j-api:$slf4jVersion")
testImplementation("org.slf4j:slf4j-simple:$slf4jVersion")
}

val javaVersion get() = JavaVersion.VERSION_17
Expand Down
2 changes: 2 additions & 0 deletions src/main/kotlin/com/github/rushyverse/api/APIPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.github.rushyverse.api
import com.github.rushyverse.api.game.SharedGameData
import com.github.rushyverse.api.koin.CraftContext
import com.github.rushyverse.api.koin.loadModule
import com.github.rushyverse.api.player.language.LanguageManager
import com.github.rushyverse.api.player.scoreboard.ScoreboardManager
import org.bukkit.Bukkit
import org.bukkit.plugin.java.JavaPlugin
Expand Down Expand Up @@ -31,6 +32,7 @@ public class APIPlugin : JavaPlugin() {
loadModule(ID_API) {
single { Bukkit.getServer() }
single { ScoreboardManager() }
single { LanguageManager() }
single { SharedGameData() }
}
}
Expand Down
74 changes: 68 additions & 6 deletions src/main/kotlin/com/github/rushyverse/api/Plugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,28 @@ import com.charleskorn.kaml.YamlConfiguration
import com.github.rushyverse.api.APIPlugin.Companion.BUNDLE_API
import com.github.rushyverse.api.configuration.reader.IFileReader
import com.github.rushyverse.api.configuration.reader.YamlFileReader
import com.github.rushyverse.api.extension.asComponent
import com.github.rushyverse.api.extension.registerListener
import com.github.rushyverse.api.koin.CraftContext
import com.github.rushyverse.api.koin.inject
import com.github.rushyverse.api.koin.loadModule
import com.github.rushyverse.api.listener.PlayerListener
import com.github.rushyverse.api.listener.VillagerListener
import com.github.rushyverse.api.player.Client
import com.github.rushyverse.api.player.ClientManager
import com.github.rushyverse.api.player.ClientManagerImpl
import com.github.rushyverse.api.player.language.LanguageManager
import com.github.rushyverse.api.serializer.*
import com.github.rushyverse.api.translation.ResourceBundleTranslator
import com.github.rushyverse.api.translation.Translator
import com.github.rushyverse.api.translation.registerResourceBundleForSupportedLocales
import com.github.shynixn.mccoroutine.bukkit.SuspendingJavaPlugin
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.SerializersModuleBuilder
import kotlinx.serialization.modules.contextual
import net.kyori.adventure.text.Component
import org.bukkit.entity.Player
import org.jetbrains.annotations.Blocking
import org.koin.core.module.Module
import org.koin.dsl.bind
import java.util.*
Expand All @@ -30,21 +36,37 @@ import java.util.*
* This abstract class provides necessary tools and life-cycle methods to facilitate the creation
* and management of a plugin that utilizes asynchronous operations, dependency injection, and
* other utility functions.
* @property id A unique identifier for this plugin.
* @property bundle The name of the resource bundle to use for this plugin.
* This ID is used for tasks like identifying the Koin application and loading Koin modules.
*/
public abstract class Plugin : SuspendingJavaPlugin() {
public abstract class Plugin(
public val id: String,
public val bundle: String
) : SuspendingJavaPlugin() {

/**
* A unique identifier for this plugin. This ID is used for tasks like identifying
* the Koin application, loading Koin modules, etc.
* Client manager linked to this plugin.
*/
public abstract val id: String
public val clientManager: ClientManager by inject(id)

/**
* Translator linked to this plugin.
*/
public val translator: Translator by inject(id)

/**
* Common language manager for all plugins.
*/
public val languageManager: LanguageManager by inject()

override suspend fun onEnableAsync() {
super.onEnableAsync()

CraftContext.startKoin(id)
moduleBukkit()
moduleClients()
moduleTranslation()

registerListener { PlayerListener(this) }
registerListener { VillagerListener(this) }
Expand All @@ -69,6 +91,15 @@ public abstract class Plugin : SuspendingJavaPlugin() {
single { ClientManagerImpl() } bind ClientManager::class
}

/**
* Creates and loads a Koin module containing translation components.
*
* @return The Koin module for translation.
*/
protected fun moduleTranslation(): Module = loadModule(id) {
single { createTranslator() } bind Translator::class
}

/**
* Creates and loads a Koin module with Bukkit-specific components.
* Can be overridden by derived classes to provide additional or customized components.
Expand Down Expand Up @@ -129,8 +160,39 @@ public abstract class Plugin : SuspendingJavaPlugin() {
*
* @return A translator configured for the supported languages.
*/
protected open suspend fun createTranslator(): ResourceBundleTranslator =
ResourceBundleTranslator().apply {
@Blocking
protected open fun createTranslator(): ResourceBundleTranslator =
ResourceBundleTranslator(bundle).apply {
registerResourceBundleForSupportedLocales(BUNDLE_API, ResourceBundle::getBundle)
}

/**
* Broadcasts a localized message to all players.
*
* This function groups players by their language preferences, translates the message once per language,
* and then sends the appropriate localized message to each player.
*
* @param players The players to whom the message should be sent.
* @param key The key used to look up the translation in the resource bundle.
* @param bundle The resource bundle to use for the translation.
* @param argumentBuilder A function that builds the arguments for the translation.
* @param messageModifier A function that modifies the translated message before it is sent.
* The modification must be chained.
*/
public suspend inline fun broadcast(
players: Collection<Player>,
key: String,
bundle: String = this.bundle,
messageModifier: (Component) -> Component = { it },
argumentBuilder: Translator.(Locale) -> Array<Any> = { emptyArray() },
) {
players.groupBy { languageManager.get(it).locale }
.forEach { (lang, receiver) ->
val translatedComponent = translator
.get(key, lang, translator.argumentBuilder(lang), bundle)
.asComponent().let(messageModifier)

receiver.forEach { it.sendMessage(translatedComponent) }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import org.bukkit.command.CommandSender
* @receiver Sender that will receive the message.
* @param message Message.
*/
public fun CommandSender.sendMessageError(message: String){
public fun CommandSender.sendMessageError(message: String) {
sendMessage(text {
content(message)
color(NamedTextColor.RED)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import java.util.*
/**
* MiniMessage instance to deserialize components without strict mode.
*/
private val MINI_MESSAGE_NON_STRICT: MiniMessage = MiniMessage.builder()
public val MINI_MESSAGE_NON_STRICT: MiniMessage = MiniMessage.builder()
.strict(false)
.tags(StandardTags.defaults())
.build()
Expand Down Expand Up @@ -231,9 +231,10 @@ public fun String.toFormattedLoreSequence(lineLength: Int = DEFAULT_LORE_LINE_LE
* The [tagResolver] will be used to resolve the custom tags and replace values.
* @receiver The string used to create the component.
* @param tagResolver The tag resolver used to resolve the custom tags.
* @param miniMessage The mini message instance used to parse the string.
* @return The component created from the string.
*/
public fun String.asComponent(
vararg tagResolver: TagResolver,
miniMessage: MiniMessage = MINI_MESSAGE_NON_STRICT
miniMessage: MiniMessage = MINI_MESSAGE_NON_STRICT,
): Component = miniMessage.deserialize(this, *tagResolver)
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,5 @@ public enum class TeamType(
public fun name(
translator: Translator,
locale: Locale = SupportedLanguage.ENGLISH.locale
): String = translator.translate("team.${name.lowercase()}", locale, BUNDLE_API)
): String = translator.get("team.${name.lowercase()}", locale, bundleName = BUNDLE_API)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.github.rushyverse.api.koin.inject
import com.github.rushyverse.api.player.Client
import com.github.rushyverse.api.player.ClientManager
import com.github.rushyverse.api.player.exception.ClientAlreadyExistsException
import com.github.rushyverse.api.player.language.LanguageManager
import com.github.rushyverse.api.player.scoreboard.ScoreboardManager
import kotlinx.coroutines.cancel
import org.bukkit.entity.Player
Expand All @@ -25,8 +26,11 @@ public class PlayerListener(
) : Listener {

private val clients: ClientManager by inject(plugin.id)

private val scoreboardManager: ScoreboardManager by inject()

private val languageManager: LanguageManager by inject()

/**
* Handle the join event to create and store a new client.
* The client will be linked to the player.
Expand Down Expand Up @@ -61,6 +65,7 @@ public class PlayerListener(
public suspend fun onQuit(event: PlayerQuitEvent) {
val player = event.player
scoreboardManager.remove(player)
languageManager.remove(player)

val client = clients.removeClient(player) ?: return
client.cancel(SilentCancellationException("The player ${player.name} (${player.uniqueId}) left"))
Expand Down
13 changes: 9 additions & 4 deletions src/main/kotlin/com/github/rushyverse/api/player/Client.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.github.rushyverse.api.delegate.DelegatePlayer
import com.github.rushyverse.api.extension.asComponent
import com.github.rushyverse.api.koin.inject
import com.github.rushyverse.api.player.exception.PlayerNotFoundException
import com.github.rushyverse.api.player.language.LanguageManager
import com.github.rushyverse.api.player.scoreboard.ScoreboardManager
import com.github.rushyverse.api.translation.SupportedLanguage
import fr.mrmicky.fastboard.adventure.FastBoard
Expand All @@ -21,14 +22,12 @@ import java.util.*
public open class Client(
public val playerUUID: UUID,
coroutineScope: CoroutineScope,
/**
* The current language of the player.
*/
public var lang: SupportedLanguage = SupportedLanguage.ENGLISH
) : CoroutineScope by coroutineScope {

private val scoreboardManager: ScoreboardManager by inject()

private val languageManager: LanguageManager by inject()

public val player: Player? by DelegatePlayer(playerUUID)

/**
Expand Down Expand Up @@ -69,4 +68,10 @@ public open class Client(
*/
public suspend fun scoreboard(): FastBoard = scoreboardManager.getOrCreate(requirePlayer())

/**
* Get the language of the player.
* @return The language of the player.
*/
public suspend fun lang(): SupportedLanguage = languageManager.get(requirePlayer())

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.github.rushyverse.api.player.language

import com.github.rushyverse.api.translation.SupportedLanguage
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.bukkit.entity.Player

/**
* Manages the languages for players within the game.
* This class ensures thread-safe operations on the languages by using mutex locks.
*/
public class LanguageManager {

/**
* Mutex used to ensure thread-safe operations on the language map.
*/
private val mutex = Mutex()

/**
* Private mutable map storing languages associated with player names.
*/
private val _languages = mutableMapOf<String, SupportedLanguage>()

/**
* Public immutable view of the languages map.
*/
public val languages: Map<String, SupportedLanguage> = _languages

/**
* Retrieves the languages for the specified player or creates a new one if it doesn't exist.
* This function is thread-safe and uses mutex locks to ensure atomic operations.
*
* @param player The player for whom the language is to be retrieved or created.
* @return The language associated with the player.
*/
public suspend fun get(player: Player): SupportedLanguage = mutex.withLock {
_languages.getOrDefault(player.name, SupportedLanguage.ENGLISH)
}

/**
* Sets the language for the specified player.
* @param player The player for whom the language is to be set.
* @param lang The language to set.
*/
public suspend fun set(player: Player, lang: SupportedLanguage) {
mutex.withLock { _languages[player.name] = lang }
}

/**
* Removes and deletes the language associated with the specified player.
* This function is thread-safe and uses mutex locks to ensure atomic operations.
*
* @param player The player whose language is to be removed.
*/
public suspend fun remove(player: Player) {
mutex.withLock { _languages.remove(player.name) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,29 +82,29 @@ public object ItemStackSerializer : KSerializer<ItemStack> {
encodeSerializableElement(descriptor, 1, amountSerializer, value.amount)
encodeSerializableElement(descriptor, 2, enchantmentsSerializer, value.enchantments)

if(itemMeta == null) return@encodeStructure
if (itemMeta == null) return@encodeStructure

encodeSerializableElement(descriptor, 3, unbreakableSerializer, itemMeta.isUnbreakable)
encodeSerializableElement(descriptor, 4, customModelSerializer, itemMeta.let {
if(it.hasCustomModelData()) it.customModelData else null
if (it.hasCustomModelData()) it.customModelData else null
})
encodeSerializableElement(
descriptor,
5,
destroyableKeysSerializer,
itemMeta.let { if(it.hasDestroyableKeys()) it.destroyableKeys.toList() else null }
itemMeta.let { if (it.hasDestroyableKeys()) it.destroyableKeys.toList() else null }
)
encodeSerializableElement(
descriptor,
6,
placeableKeysSerializer,
itemMeta.let { if(it.hasPlaceableKeys()) it.placeableKeys.toList() else null }
itemMeta.let { if (it.hasPlaceableKeys()) it.placeableKeys.toList() else null }
)
encodeSerializableElement(descriptor, 7, displayNameSerializer, itemMeta.let {
if(it.hasDisplayName()) it.displayName() else null
if (it.hasDisplayName()) it.displayName() else null
})
encodeSerializableElement(descriptor, 8, loreSerializer, itemMeta.let {
if(it.hasLore()) it.lore() else null
if (it.hasLore()) it.lore() else null
})
encodeSerializableElement(
descriptor,
Expand Down
Loading