diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-01-17 15:10:36 +0900 |
|---|---|---|
| committer | Sho Sakuma <me@m1sk9.dev> | 2026-01-17 15:10:36 +0900 |
| commit | 5a37ad56989ea3d2fdc163daa9215c90595b326d (patch) | |
| tree | e005e54d7c70cc0c9f905f1ab1278e3205e31332 /engine | |
| parent | b54e0eca7a49f98eb3f4a976d98bf8b3f8a40dd5 (diff) | |
| download | LunaticChat-5a37ad56989ea3d2fdc163daa9215c90595b326d.tar.gz LunaticChat-5a37ad56989ea3d2fdc163daa9215c90595b326d.tar.bz2 LunaticChat-5a37ad56989ea3d2fdc163daa9215c90595b326d.zip | |
refactor: Move modules without dependencies to the engine
Diffstat (limited to 'engine')
9 files changed, 513 insertions, 1 deletions
diff --git a/engine/build.gradle.kts b/engine/build.gradle.kts index 60c5c7b..9af6eb8 100644 --- a/engine/build.gradle.kts +++ b/engine/build.gradle.kts @@ -3,4 +3,10 @@ plugins { kotlin("plugin.serialization") } -dependencies {} +dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") + implementation("io.ktor:ktor-client-core:3.3.3") + implementation("io.ktor:ktor-client-cio:3.3.3") + compileOnly("net.kyori:adventure-api:4.18.0") +} diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/command/CommandResult.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/command/CommandResult.kt new file mode 100644 index 0000000..9c9d882 --- /dev/null +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/command/CommandResult.kt @@ -0,0 +1,37 @@ +package dev.m1sk9.lunaticChat.engine.command + +import net.kyori.adventure.text.Component + +/** + * Represents the result of a command execution. + * Uses Kotlin sealed classes for type-safe result handling. + */ +sealed class CommandResult { + /** Command executed successfully */ + data object Success : CommandResult() + + /** Command executed successfully with a message to display */ + data class SuccessWithMessage( + val message: Component, + ) : CommandResult() + + /** Command failed with an error message */ + data class Failure( + val message: Component, + ) : CommandResult() + + /** Command failed due to invalid usage */ + data class InvalidUsage( + val usageHint: String, + ) : CommandResult() + + /** + * Converts result to Brigadier return value. + * @return 1 for success, 0 for failure (Brigadier convention) + */ + fun toBrigadierResult(): Int = + when (this) { + is Success, is SuccessWithMessage -> 1 + is Failure, is InvalidUsage -> 0 + } +} diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/converter/CacheData.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/converter/CacheData.kt new file mode 100644 index 0000000..1f335b4 --- /dev/null +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/converter/CacheData.kt @@ -0,0 +1,9 @@ +package dev.m1sk9.lunaticChat.engine.converter + +import kotlinx.serialization.Serializable + +@Serializable +data class CacheData( + val version: String, + val entries: Map<String, String>, +) diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/converter/GoogleIMEClient.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/converter/GoogleIMEClient.kt new file mode 100644 index 0000000..4a23bc7 --- /dev/null +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/converter/GoogleIMEClient.kt @@ -0,0 +1,63 @@ +package dev.m1sk9.lunaticChat.engine.converter + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.get +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +class GoogleIMEClient( + private val timeout: Duration = 3.seconds, + private val httpClient: HttpClient, +) { + suspend fun convert(input: String): String = + withContext(Dispatchers.IO) { + withTimeout(timeout) { + val res = + httpClient.get("https://www.google.com/transliterate") { + url { + parameters.append("langpair", "ja-Hira|ja") + parameters.append("text", input) + } + } + + if (res.status != io.ktor.http.HttpStatusCode.OK) { + throw Exception("Google IME API returned status code: ${res.status.value}") + } + + val jsonData = res.body<String>() + parseResponse(jsonData) + } + } + + private fun parseResponse(data: String): String { + val parsed = Json.parseToJsonElement(data) + val resultArray = parsed.jsonArray + + // Response format: [["input", ["candidate1", "candidate2", ...]], ...] + // For multi-word input, there are multiple arrays, one per segment + val result = StringBuilder() + + for (segment in resultArray) { + val segmentArray = segment.jsonArray + if (segmentArray.size >= 2) { + val candidates = segmentArray[1].jsonArray + if (candidates.isNotEmpty()) { + result.append(candidates[0].jsonPrimitive.content) + } + } + } + + if (result.isEmpty()) { + throw IllegalStateException("No conversion result found in the response.") + } + + return result.toString() + } +} diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/converter/KanaConverter.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/converter/KanaConverter.kt new file mode 100644 index 0000000..47ab960 --- /dev/null +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/converter/KanaConverter.kt @@ -0,0 +1,297 @@ +package dev.m1sk9.lunaticChat.engine.converter + +/** + * Converts romanji text to hiragana using Trie data structure. + */ +object KanaConverter { + sealed class TrieNode { + data class Leaf( + val value: String, + ) : TrieNode() + + data class Branch( + val children: Map<Char, TrieNode>, + val value: String? = null, + ) : TrieNode() + } + + private val romanjiTrie: TrieNode = buildTrie() + + private fun buildTrie(): TrieNode { + val mappings = + listOf( + // 3文字変換 + "kya" to "きゃ", + "kyi" to "きぃ", + "kyu" to "きゅ", + "kye" to "きぇ", + "kyo" to "きょ", + "gya" to "ぎゃ", + "gyi" to "ぎぃ", + "gyu" to "ぎゅ", + "gye" to "ぎぇ", + "gyo" to "ぎょ", + "sha" to "しゃ", + "shi" to "し", + "shu" to "しゅ", + "she" to "しぇ", + "sho" to "しょ", + "sya" to "しゃ", + "syi" to "しぃ", + "syu" to "しゅ", + "sye" to "しぇ", + "syo" to "しょ", + "zya" to "じゃ", + "zyi" to "じぃ", + "zyu" to "じゅ", + "zye" to "じぇ", + "zyo" to "じょ", + "jya" to "じゃ", + "jyi" to "じぃ", + "jyu" to "じゅ", + "jye" to "じぇ", + "jyo" to "じょ", + "cha" to "ちゃ", + "chi" to "ち", + "chu" to "ちゅ", + "che" to "ちぇ", + "cho" to "ちょ", + "tya" to "ちゃ", + "tyi" to "ちぃ", + "tyu" to "ちゅ", + "tye" to "ちぇ", + "tyo" to "ちょ", + "dya" to "ぢゃ", + "dyi" to "ぢぃ", + "dyu" to "ぢゅ", + "dye" to "ぢぇ", + "dyo" to "ぢょ", + "nya" to "にゃ", + "nyi" to "にぃ", + "nyu" to "にゅ", + "nye" to "にぇ", + "nyo" to "にょ", + "hya" to "ひゃ", + "hyi" to "ひぃ", + "hyu" to "ひゅ", + "hye" to "ひぇ", + "hyo" to "ひょ", + "bya" to "びゃ", + "byi" to "びぃ", + "byu" to "びゅ", + "bye" to "びぇ", + "byo" to "びょ", + "pya" to "ぴゃ", + "pyi" to "ぴぃ", + "pyu" to "ぴゅ", + "pye" to "ぴぇ", + "pyo" to "ぴょ", + "mya" to "みゃ", + "myi" to "みぃ", + "myu" to "みゅ", + "mye" to "みぇ", + "myo" to "みょ", + "rya" to "りゃ", + "ryi" to "りぃ", + "ryu" to "りゅ", + "rye" to "りぇ", + "ryo" to "りょ", + "tsu" to "つ", + "thi" to "てぃ", + "dhi" to "でぃ", + "dhu" to "でゅ", + "wha" to "うぁ", + "whi" to "うぃ", + "whe" to "うぇ", + "who" to "うぉ", + // 2文字変換 + "ka" to "か", + "ki" to "き", + "ku" to "く", + "ke" to "け", + "ko" to "こ", + "ga" to "が", + "gi" to "ぎ", + "gu" to "ぐ", + "ge" to "げ", + "go" to "ご", + "sa" to "さ", + "si" to "し", + "su" to "す", + "se" to "せ", + "so" to "そ", + "za" to "ざ", + "zi" to "じ", + "zu" to "ず", + "ze" to "ぜ", + "zo" to "ぞ", + "ja" to "じゃ", + "ji" to "じ", + "ju" to "じゅ", + "je" to "じぇ", + "jo" to "じょ", + "ta" to "た", + "ti" to "ち", + "tu" to "つ", + "te" to "て", + "to" to "と", + "da" to "だ", + "di" to "ぢ", + "du" to "づ", + "de" to "で", + "do" to "ど", + "na" to "な", + "ni" to "に", + "nu" to "ぬ", + "ne" to "ね", + "no" to "の", + "ha" to "は", + "hi" to "ひ", + "hu" to "ふ", + "he" to "へ", + "ho" to "ほ", + "fu" to "ふ", + "ba" to "ば", + "bi" to "び", + "bu" to "ぶ", + "be" to "べ", + "bo" to "ぼ", + "pa" to "ぱ", + "pi" to "ぴ", + "pu" to "ぷ", + "pe" to "ぺ", + "po" to "ぽ", + "ma" to "ま", + "mi" to "み", + "mu" to "む", + "me" to "め", + "mo" to "も", + "ya" to "や", + "yi" to "い", + "yu" to "ゆ", + "ye" to "いぇ", + "yo" to "よ", + "ra" to "ら", + "ri" to "り", + "ru" to "る", + "re" to "れ", + "ro" to "ろ", + "wa" to "わ", + "wi" to "ゐ", + "wu" to "う", + "we" to "ゑ", + "wo" to "を", + "la" to "ら", + "li" to "り", + "lu" to "る", + "le" to "れ", + "lo" to "ろ", + "nn" to "ん", + // 1文字変換 + "a" to "あ", + "i" to "い", + "u" to "う", + "e" to "え", + "o" to "お", + "n" to "ん", + ) + + return insertAll(TrieNode.Branch(emptyMap()), mappings) + } + + private fun insertAll( + root: TrieNode, + mappings: List<Pair<String, String>>, + ): TrieNode { + var current = root + for ((key, value) in mappings) { + current = insert(current, key, value) + } + return current + } + + private fun insert( + node: TrieNode, + key: String, + value: String, + ): TrieNode { + if (key.isEmpty()) { + return when (node) { + is TrieNode.Branch -> TrieNode.Branch(node.children, value) + is TrieNode.Leaf -> TrieNode.Leaf(value) + } + } + + return when (node) { + is TrieNode.Branch -> { + val char = key[0] + val child = node.children[char] ?: TrieNode.Branch(emptyMap()) + val newChild = insert(child, key.substring(1), value) + TrieNode.Branch(node.children + (char to newChild), node.value) + } + is TrieNode.Leaf -> node + } + } + + /** + * Converts romanji text to hiragana. + * + * @param input The romanji text to convert + * @return The converted hiragana text + */ + fun toHiragana(input: String): String { + val result = StringBuilder() + var i = 0 + val lowerInput = input.lowercase() + + while (i < lowerInput.length) { + if (i + 1 < lowerInput.length) { + val current = lowerInput[i] + val next = lowerInput[i + 1] + if (current == next && current in "bcdfghjklmpqrstvwxyz") { + result.append('っ') + i++ + continue + } + } + + var node: TrieNode = romanjiTrie + var lastMatch: Pair<String, Int>? = null + var j = i + + while (j < lowerInput.length) { + node = + when (node) { + is TrieNode.Branch -> { + if (node.value != null) { + lastMatch = node.value to (j - i) + } + + node.children[lowerInput[j]] ?: break + } + is TrieNode.Leaf -> { + lastMatch = node.value to (j - i) + break + } + } + j++ + } + + if (node is TrieNode.Leaf) { + lastMatch = node.value to (j - i) + } else if (node is TrieNode.Branch && node.value != null) { + lastMatch = node.value to (j - i) + } + + if (lastMatch != null) { + result.append(lastMatch.first) + i += lastMatch.second + } else { + result.append(lowerInput[i]) + i++ + } + } + + return result.toString() + } +} diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerChatSettings.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerChatSettings.kt new file mode 100644 index 0000000..12eda6a --- /dev/null +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerChatSettings.kt @@ -0,0 +1,17 @@ +package dev.m1sk9.lunaticChat.engine.settings + +import kotlinx.serialization.Serializable +import java.util.UUID + +/** + * Data model for per-player chat settings. + * + * @property uuid The unique identifier of the player + * @property japaneseConversionEnabled Whether romaji-to-Japanese conversion is enabled for this player + */ +@Serializable +data class PlayerChatSettings( + @Serializable(with = UUIDSerializer::class) + val uuid: UUID, + val japaneseConversionEnabled: Boolean = true, +) diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerSettingsData.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerSettingsData.kt new file mode 100644 index 0000000..1fc3699 --- /dev/null +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerSettingsData.kt @@ -0,0 +1,29 @@ +package dev.m1sk9.lunaticChat.engine.settings + +import kotlinx.serialization.Serializable +import java.util.UUID + +/** + * Root data structure for player settings stored in YAML format. + * All player settings are stored in a single YAML file with this structure. + * + * Example YAML format: + * ```yaml + * version: 1 + * japaneseConversion: + * ceaea267-39dd-3bac-931c-761ada671ebe: true + * another-uuid-here: false + * ``` + * + * @property version The schema version for future compatibility + * @property japaneseConversion Map of player UUIDs to their Japanese conversion enabled status + */ +@Serializable +data class PlayerSettingsData( + val version: Int = 1, + val japaneseConversion: Map< + @Serializable(with = UUIDASStringSerializer::class) + UUID, + Boolean, + > = emptyMap(), +) diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/UUIDASStringSerializer.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/UUIDASStringSerializer.kt new file mode 100644 index 0000000..7e6b331 --- /dev/null +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/UUIDASStringSerializer.kt @@ -0,0 +1,27 @@ +package dev.m1sk9.lunaticChat.engine.settings + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import java.util.UUID + +/** + * Serializer for UUID that converts to/from String format for YAML compatibility. + * Used in PlayerSettingsData for serializing UUID keys in maps. + */ +object UUIDASStringSerializer : KSerializer<UUID> { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("UUIDAsString", PrimitiveKind.STRING) + + override fun serialize( + encoder: Encoder, + value: UUID, + ) { + encoder.encodeString(value.toString()) + } + + override fun deserialize(decoder: Decoder): UUID = UUID.fromString(decoder.decodeString()) +} diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/UUIDSerializer.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/UUIDSerializer.kt new file mode 100644 index 0000000..e03c1c1 --- /dev/null +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/UUIDSerializer.kt @@ -0,0 +1,27 @@ +package dev.m1sk9.lunaticChat.engine.settings + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import java.util.UUID + +/** + * Custom serializer for UUID with kotlinx.serialization. + * kotlinx.serialization doesn't support UUID by default, so we need a custom serializer. + */ +object UUIDSerializer : KSerializer<UUID> { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): UUID = UUID.fromString(decoder.decodeString()) + + override fun serialize( + encoder: Encoder, + value: UUID, + ) { + encoder.encodeString(value.toString()) + } +} |
