summaryrefslogtreecommitdiff
path: root/engine
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-01-17 15:10:36 +0900
committerSho Sakuma <me@m1sk9.dev>2026-01-17 15:10:36 +0900
commit5a37ad56989ea3d2fdc163daa9215c90595b326d (patch)
treee005e54d7c70cc0c9f905f1ab1278e3205e31332 /engine
parentb54e0eca7a49f98eb3f4a976d98bf8b3f8a40dd5 (diff)
downloadLunaticChat-5a37ad56989ea3d2fdc163daa9215c90595b326d.tar.gz
LunaticChat-5a37ad56989ea3d2fdc163daa9215c90595b326d.tar.bz2
LunaticChat-5a37ad56989ea3d2fdc163daa9215c90595b326d.zip
refactor: Move modules without dependencies to the engine
Diffstat (limited to 'engine')
-rw-r--r--engine/build.gradle.kts8
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/command/CommandResult.kt37
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/converter/CacheData.kt9
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/converter/GoogleIMEClient.kt63
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/converter/KanaConverter.kt297
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerChatSettings.kt17
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerSettingsData.kt29
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/UUIDASStringSerializer.kt27
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/UUIDSerializer.kt27
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())
+ }
+}