diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 62fc8f1..840cc2a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -48,7 +48,7 @@ protocolLib = "6845acd89d" cloudnet = "4.0.0-RC10" luckperms = "5.4" - +kyoriAdventure = "6.2.0" [libraries] @@ -105,6 +105,9 @@ playerAnimator = { group = "dev.kosmx.player-anim", name = "player-animation-lib geckolib = { group = "software.bernie.geckolib", name = "geckolib-fabric-1.21.4", version.ref = "geckolib" } emoteLib = { group = "gg.norisk", name = "emote-lib", version = "1.21.4-1.1.20" } +# kyori +kyoriAdventure = { group = "net.kyori", name = "adventure-platform-fabric", version.ref = "kyoriAdventure" } + # cloudnet cloudnet-driver = { module = "eu.cloudnetservice.cloudnet:driver", version.ref = "cloudnet" } cloudnet-bridge = { module = "eu.cloudnetservice.cloudnet:bridge", version.ref = "cloudnet" } diff --git a/hero-api/build.gradle.kts b/hero-api/build.gradle.kts index f2ca1af..5ab6369 100644 --- a/hero-api/build.gradle.kts +++ b/hero-api/build.gradle.kts @@ -16,4 +16,5 @@ dependencies { modApi(libs.owolib) modApi(libs.geckolib) modApi(libs.emoteLib) + modApi(libs.kyoriAdventure) } diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/HeroesManager.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/HeroesManager.kt index 273c653..8987f16 100644 --- a/hero-api/src/main/kotlin/gg/norisk/heroes/common/HeroesManager.kt +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/HeroesManager.kt @@ -1,5 +1,6 @@ package gg.norisk.heroes.common +import gg.norisk.heroes.common.localization.LocalizationManager import gg.norisk.heroes.common.registry.SoundRegistry import gg.norisk.heroes.server.HeroesManagerServer import net.fabricmc.api.EnvType @@ -38,6 +39,7 @@ object HeroesManager : ModInitializer { logger.info("Init Hero-Api Common...") SoundRegistry.init() HeroesManagerServer.initServer() + LocalizationManager.init() ServerLifecycleEvents.SERVER_STARTING.register { setBasePath(it.getSavePath(WorldSavePath("heroes"))) diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/localization/LocalizationManager.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/localization/LocalizationManager.kt new file mode 100644 index 0000000..65b8521 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/localization/LocalizationManager.kt @@ -0,0 +1,94 @@ +package gg.norisk.heroes.common.localization + +import gg.norisk.heroes.common.HeroesManager +import gg.norisk.heroes.common.localization.minimessage.MiniMessageUtils +import gg.norisk.heroes.common.utils.createIfNotExists +import net.fabricmc.api.EnvType +import net.fabricmc.api.Environment +import net.fabricmc.loader.api.FabricLoader +import net.minecraft.client.network.ClientPlayerEntity +import net.minecraft.client.resource.language.I18n +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.server.network.ServerPlayerEntity +import net.minecraft.text.Text +import net.silkmc.silk.core.logging.logger +import net.silkmc.silk.core.text.literalText +import java.util.* +import kotlin.io.path.Path +import kotlin.io.path.absolutePathString + +object LocalizationManager { + val defaultLocale: Locale = Locale.ENGLISH + + fun init() { + loadAllLanguageFiles() + } + + private fun loadAllLanguageFiles() { + if (FabricLoader.getInstance().environmentType == EnvType.SERVER) { + FabricLoader.getInstance().getModContainer(HeroesManager.MOD_ID).map { container -> + val modAssets = container.rootPath.toFile().resolve("assets/${HeroesManager.MOD_ID}") + val langDir = modAssets.resolve("lang") + + langDir.listFiles()?.forEach { langFile -> + val locale = runCatching { Locale.of(langFile.nameWithoutExtension) } + .onFailure { + print("Localization error: ") + it.printStackTrace() + }.getOrNull() ?: defaultLocale + logger().info("Found lang file for locale $locale") + LocalizationRegistry.register(locale).registerAllFromFile(langFile) + } + } + } + } + + @Environment(EnvType.SERVER) + fun getLocalizedTextForPlayerServer(player: ServerPlayerEntity, key: String, vararg args: Any): Text { + val language = player.clientOptions.language + val locale = Locale.of(language) + val translations = LocalizationRegistry.get(locale) + if (translations == null) { + HeroesManager.logger.warn("translationRegistry for locale `${locale}` does not exist. all locales: ${LocalizationRegistry.locales.keys}") + } + + val minimessage = translations?.getAsMinimessage(key, *args) + if (minimessage == null) { + HeroesManager.logger.warn("Translation for key `$key` does not exist.") + } + val text = minimessage + ?: literalText { + text(key) + if (args.isNotEmpty()) { + text(" ${args.joinToString()}") + } + } + return text + } + + @Environment(EnvType.CLIENT) + fun getLocalizedTextOnClient(key: String, vararg args: Any): Text { + val translatedText = I18n.translate(key, *args) + val formatted = MiniMessageUtils.deserialize(translatedText) + return formatted + } +} + +@Environment(EnvType.CLIENT) +fun localizedText(key: String, vararg args: Any): Text { + return LocalizationManager.getLocalizedTextOnClient(key, *args) +} + +fun PlayerEntity.getLocalized(key: String, vararg args: Any): Text { + return if (this is ClientPlayerEntity) { + println("is client player") + LocalizationManager.getLocalizedTextOnClient(key, *args) + } else { + println("is server player") + LocalizationManager.getLocalizedTextForPlayerServer(this as ServerPlayerEntity, key, *args) + } +} + +fun PlayerEntity.sendLocalized(key: String, vararg args: Any) { + sendMessage(this.getLocalized(key, *args), false) +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/localization/LocalizationRegistry.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/localization/LocalizationRegistry.kt new file mode 100644 index 0000000..e561b10 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/localization/LocalizationRegistry.kt @@ -0,0 +1,22 @@ +package gg.norisk.heroes.common.localization + +import java.util.Locale + +object LocalizationRegistry { + val locales = hashMapOf() + + fun register(locale: Locale): TranslationRegistry { + val translationRegistry = TranslationRegistry() + locales[locale] = translationRegistry + return translationRegistry + } + + fun register(language: String): TranslationRegistry { + val locale = Locale.of(language) + return register(locale) + } + + fun get(locale: Locale): TranslationRegistry? { + return locales[locale] ?: locales[LocalizationManager.defaultLocale] + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/localization/TranslationRegistry.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/localization/TranslationRegistry.kt new file mode 100644 index 0000000..e8e758f --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/localization/TranslationRegistry.kt @@ -0,0 +1,50 @@ +package gg.norisk.heroes.common.localization + +import gg.norisk.heroes.common.localization.minimessage.minimessage +import kotlinx.serialization.json.Json +import net.minecraft.text.Text +import java.io.File + +class TranslationRegistry { + val translations: HashMap = hashMapOf() + + fun register(key: String, value: String) { + translations[key] = value + } + + fun register(translation: Pair) { + val (key, value) = translation + return register(key, value) + } + + fun registerAllFromFile(file: File) { + val fileContent = file.readText() + val fileTranslations = runCatching { + Json.decodeFromString>(fileContent) + }.getOrNull() ?: hashMapOf() + + fileTranslations.forEach { (key, value) -> + register(key, value) + } + } + + fun get(key: String): String { + return translations[key] ?: key + } + + fun getFormatted(key: String, vararg args: Any): String { + val string = get(key) + val formattedString = string.format(*args) + return formattedString + } + + fun getAsText(key: String, vararg args: Any): Text { + val formattedString = getFormatted(key, *args) + return Text.literal(formattedString) + } + + fun getAsMinimessage(key: String, vararg args: Any): Text { + val formattedString = getFormatted(key, *args) + return minimessage(formattedString) + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/localization/minimessage/LaborColorTagResolver.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/localization/minimessage/LaborColorTagResolver.kt new file mode 100644 index 0000000..e6cdb87 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/localization/minimessage/LaborColorTagResolver.kt @@ -0,0 +1,94 @@ +package gg.norisk.heroes.common.localization.minimessage + +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.format.Style +import net.kyori.adventure.text.format.TextColor +import net.kyori.adventure.text.minimessage.Context +import net.kyori.adventure.text.minimessage.ParsingException +import net.kyori.adventure.text.minimessage.internal.serializer.SerializableResolver +import net.kyori.adventure.text.minimessage.internal.serializer.StyleClaim +import net.kyori.adventure.text.minimessage.internal.serializer.TokenEmitter +import net.kyori.adventure.text.minimessage.tag.Tag +import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver + +internal class LaborColorTagResolver : TagResolver, SerializableResolver.Single { + @Throws(ParsingException::class) + override fun resolve(name: String, args: ArgumentQueue, ctx: Context): Tag? { + if (!this.has(name)) { + return null + } + val colorName = if (isLaborColorOrAbbreviation(name)) { + args.popOr("Expected to find a color parameter: |#RRGGBB").lowerValue() + } else { + name + } + + val color = resolveColor(colorName, ctx) + return Tag.styling(color) + } + + override fun has(name: String): Boolean { + return isLaborColorOrAbbreviation(name) || COLOR_ALIASES.containsKey(name) + } + + override fun claimStyle(): StyleClaim<*>? { + return STYLE + } + + companion object { + private const val LABORCOLOR_3 = "lc" + private const val LABORCOLOR_2 = "laborColour" + private const val LABORCOLOR = "laborColor" + val laborColors = hashMapOf() + + val INSTANCE: TagResolver = LaborColorTagResolver() + private val STYLE = StyleClaim.claim( + LABORCOLOR, + { obj: Style -> obj.color() }, + { color: TextColor, emitter: TokenEmitter -> + // TODO: custom aliases + // TODO: compact vs expanded format? COLOR vs color:COLOR vs c:COLOR + if (color is NamedTextColor) { + emitter.tag(NamedTextColor.NAMES.key(color)!!) + } else { + emitter.tag(color.asHexString()) + } + }) + + private val COLOR_ALIASES: MutableMap = HashMap() + + init { + LaborColors.getAllColorsWithValue().forEach { color, value -> + laborColors[color] = value + } + } + + private fun isLaborColorOrAbbreviation(name: String): Boolean { + return name == LABORCOLOR || name == LABORCOLOR_2 || name == LABORCOLOR_3 + } + + @Throws(ParsingException::class) + fun resolveColor(colorName: String, ctx: Context): TextColor { + val color = if (laborColors.containsKey(colorName.lowercase())) { + TextColor.color(laborColors[colorName]!!) + } else if (COLOR_ALIASES.containsKey(colorName)) { + COLOR_ALIASES[colorName] + } else if (colorName[0] == TextColor.HEX_CHARACTER) { + TextColor.fromHexString(colorName) + } else { + NamedTextColor.NAMES.value(colorName) + } + + if (color == null) { + throw ctx.newException( + String.format( + "Unable to parse a color from '%s'. Please use named.", + colorName + ) + ) + } + return color + } + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/localization/minimessage/LaborColors.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/localization/minimessage/LaborColors.kt new file mode 100644 index 0000000..0b6f5d1 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/localization/minimessage/LaborColors.kt @@ -0,0 +1,32 @@ +package gg.norisk.heroes.common.localization.minimessage + +import kotlin.reflect.full.declaredMemberProperties + +object LaborColors { + val White = 0xFFFFFF + val Gray = 0xAAAAAA + val DarkGray = 0x555555 + val Pink = 0xEF6F82 + val LightPink = 0xFFC0CB + val DarkPink = 0x966f76 + val DarkPurple = 0x8C5ABB + val Purple = 0xD9C4EC + val Green = 0xBFFFB7 + val DarkGreen = 0x0E7C00 + val Red = 0xFF9997 + val DarkRed = 0xFF4C49 + val Blue = 0x3399ff + val LightBlue = 0xA6EDFF + val Yellow = 0xF9E795 + val Orange = 0xFFB797 + val CornSilk = 0xFFF8DC + val DarkerCornSilk = 0x858276 + + fun getAllColorsWithValue(): Map { + return this::class.declaredMemberProperties.associate { + val name = it.name.lowercase() + val value = it.getter.call(this) as Int + name to value + } + } +} diff --git a/hero-api/src/main/kotlin/gg/norisk/heroes/common/localization/minimessage/MiniMessageUtils.kt b/hero-api/src/main/kotlin/gg/norisk/heroes/common/localization/minimessage/MiniMessageUtils.kt new file mode 100644 index 0000000..628c5f7 --- /dev/null +++ b/hero-api/src/main/kotlin/gg/norisk/heroes/common/localization/minimessage/MiniMessageUtils.kt @@ -0,0 +1,39 @@ +package gg.norisk.heroes.common.localization.minimessage + +import net.fabricmc.api.EnvType +import net.fabricmc.loader.api.FabricLoader +import net.kyori.adventure.platform.modcommon.MinecraftClientAudiences +import net.kyori.adventure.platform.modcommon.MinecraftServerAudiences +import net.kyori.adventure.text.minimessage.MiniMessage +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver +import net.kyori.adventure.text.minimessage.tag.standard.* +import net.minecraft.text.Text +import net.silkmc.silk.core.Silk + +object MiniMessageUtils { + private val audiences by lazy { + if (FabricLoader.getInstance().environmentType == EnvType.SERVER) { + MinecraftServerAudiences.builder(Silk.serverOrThrow).build() + } else { + MinecraftClientAudiences.builder().build() + } + } + + private val miniMessage = MiniMessage.miniMessage() + + private val tagResolver = TagResolver.builder().resolvers( + StandardTags.defaults(), + LaborColorTagResolver.INSTANCE + ).build() + + + fun deserialize(string: String): Text { + val miniMessageComponent = miniMessage.deserialize(string, tagResolver) + val text = audiences.asNative(miniMessageComponent) + return text + } +} + +fun minimessage(string: String): Text { + return MiniMessageUtils.deserialize(string) +}