summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-01-11 18:50:12 +0900
committerGitHub <noreply@github.com>2026-01-11 18:50:12 +0900
commitefdaff7325f6ae7abec46b64d5b4b6019c01f5f2 (patch)
tree3d7854a0b6ca9800e475cc9577149475875c57ae
parentae0796559160430b5d251543ec27184467f32d8e (diff)
parenta7c47276162f40e7e3fa673c4afbc74e52d8c6ee (diff)
downloadLunaticChat-efdaff7325f6ae7abec46b64d5b4b6019c01f5f2.tar.gz
LunaticChat-efdaff7325f6ae7abec46b64d5b4b6019c01f5f2.tar.bz2
LunaticChat-efdaff7325f6ae7abec46b64d5b4b6019c01f5f2.zip
Merge pull request #25 from m1sk9/fix/23/change-file-format
fix!: Change user-setting file format
-rw-r--r--CHANGELOG.md10
-rw-r--r--docs/.vitepress/config.mts5
-rw-r--r--docs/src/guide/configuration.md26
-rw-r--r--docs/src/guide/getting-started.md2
-rw-r--r--docs/src/guide/japanese-romanization.md61
-rw-r--r--docs/src/static/favicon.icobin0 -> 4286 bytes
-rw-r--r--docs/src/static/icon.pngbin0 -> 8069496 bytes
-rw-r--r--platform-paper/build.gradle.kts1
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt175
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt10
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/LunaticChatConfiguration.kt1
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/JapaneseConversionFeatureConfig.kt1
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerPresenceListener.kt10
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsData.kt29
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManager.kt139
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/UUIDASStringSerializer.kt27
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt114
-rw-r--r--platform-paper/src/main/resources/config.yml10
18 files changed, 419 insertions, 202 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index abc3346..a8e3bc0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,16 @@
### v0.3.1
+#### Breaking Changes:
+
+- Change user settings file format from JSON to TOML.
+ - **As a result, players will need to re-enable the `/jp` setting**.
+ - The old JSON format user configuration files are no longer used. You can safely delete them.
+- The configuration file has been deleted or modified.
+ - It must be initialized when the server starts.
+
+----
+
- Fixed an issue where Japanese text was being converted to Romanized characters.
### v0.3.0
diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts
index 2cd9c07..4894ff9 100644
--- a/docs/.vitepress/config.mts
+++ b/docs/.vitepress/config.mts
@@ -8,6 +8,7 @@ export default defineConfig({
cleanUrls: true,
srcDir: './src',
outDir: './dist',
+ head: [['link', { rel: 'icon', href: '/static/favicon.ico' }]],
themeConfig: {
nav: [
{ text: 'Guide', link: '/guide/getting-started' },
@@ -27,6 +28,10 @@ export default defineConfig({
text: 'パーミッション',
link: '/guide/permissions',
},
+ {
+ text: 'ローマ字変換',
+ link: '/guide/japanese-romanization',
+ },
],
'/reference/': [
{
diff --git a/docs/src/guide/configuration.md b/docs/src/guide/configuration.md
index b1d606a..14177b4 100644
--- a/docs/src/guide/configuration.md
+++ b/docs/src/guide/configuration.md
@@ -12,11 +12,14 @@
# Please specify appropriate values to ensure LunaticChat functions correctly.
#
# For detailed configuration options, please refer to the documentation:
-# https://lc.m1sk9.dev/guide//configuration
+# https://lc.m1sk9.dev/guide/configuration
# ----------------------------------------------
# If enabled, Activate LunaticChat's debug mode, which provides detailed logging for troubleshooting.
-debug: false
+debug: true
+
+# Path to the YAML file storing player settings
+userSettingsFilePath: "player-settings.yaml"
# ----------------------------------------------
# ----------- Features Settings ------------
@@ -28,7 +31,7 @@ features:
enabled: true
japaneseConversion:
# If enabled, enables the conversion function from Roman letters to hiragana.
- enabled: false
+ enabled: true
cache:
maxEntries: 500
saveIntervalSeconds: 300
@@ -36,8 +39,6 @@ features:
api:
timeout: 3000
retryAttempts: 2
- settings:
- directory: "settings"
# ----------------------------------------------
# --------- Message Format Settings --------
@@ -47,6 +48,7 @@ features:
# You can use placeholders such as {sender}, {message}, etc.
#
# {sender} - The name of the message sender
+# {recipient} - The name of the message recipient
# {message} - The content of the message
# ----------------------------------------------
@@ -64,6 +66,13 @@ messageFormat:
LunaticChat をデバッグモードで起動します.
+### `userSettingsFilePath`
+
+- Type: `string`
+- Default: `player-settings.yaml`
+
+LunaticChat がプレイヤーの設定を保存する YAML ファイルのパスを指定します.
+
## Features Settings
### `features.quickReplies.enabled`
@@ -125,13 +134,6 @@ LunaticChat の [`/reply`](../reference/commands/reply.md) コマンドによる
ローマ字変換 API へのリクエストが失敗した場合の再試行回数を指定します.
-#### `settings.directory`
-
-- Type: `string`
-- Default: `settings`
-
-ローマ字変換のユーザーごとの設定ファイルを保存するディレクトリを指定します.
-
## Message Format Settings
使用できるプレースホルダー:
diff --git a/docs/src/guide/getting-started.md b/docs/src/guide/getting-started.md
index db922f6..85fbf31 100644
--- a/docs/src/guide/getting-started.md
+++ b/docs/src/guide/getting-started.md
@@ -14,7 +14,7 @@
LunaticChat を起動すると以下のファイルが作成されます.
- `plugins/LunaticChat/config.yml`:プラグインの設定ファイル
-- `plugins/LunaticChat/settings/`:ユーザーごとの設定ファイルが保存されるフォルダ
+- `plugins/LunaticChat/player-settings.yaml`:ユーザーごとの設定ファイル
- `plugins/LunaticChat/conversion_cache.json`: ローマ字変換のキャッシュファイル
設定ファイル `config.yml` を開き,必要に応じて設定を変更します.設定項目の詳細については,[設定ガイド](./configuration.md)を参照してください.
diff --git a/docs/src/guide/japanese-romanization.md b/docs/src/guide/japanese-romanization.md
new file mode 100644
index 0000000..3e9d702
--- /dev/null
+++ b/docs/src/guide/japanese-romanization.md
@@ -0,0 +1,61 @@
+# ローマ字変換
+
+LunaticChat では、日本語のテキストをローマ字に変換する機能が提供されています.
+
+## 設定を有効化する
+
+ローマ字変換機能を有効にするには `features.japaneseConversion.enabled` の値を `true` にする必要があります.
+
+```yaml{4}
+features:
+ # ...
+ japaneseConversion:
+ enabled: true
+```
+
+## 変換の仕組み
+
+LunaticChat はプレイヤーからのテキストを以下の手順で変換を行います.
+
+1. プレイヤーからのテキストがローマ字で構成されているかを検証する
+2. メモリキャッシュから該当のフレーズがあるかどうかを調べる
+ 1. ここで該当フレーズがヒットした場合はそれをサーバーに返す
+3. メモリキャッシュに存在していない場合は,ローマ字からひらがなに変換する
+4. 変換した文字列を Google IME API へ送り,人が読める形に変換する
+5. メモリキャッシュに保存し,それをサーバーに返す
+
+```
+┌───────────────────────────────────────────────────┐
+│ User Input │
+│ (Romanji Text) │
+└─────────────────────┬─────────────────────────────┘
+ │
+ ▼
+┌───────────────────────────────────────────────────┐
+│ RomanjiConverter │
+│ ┌───────────────────────────────────────────┐ │
+│ │ 1. Check Memory Cache │ │
+│ │ └─→ Hit: Return immediately │ │
+│ │ │ │
+│ │ 2. Call Google IME API │ │
+│ │ │ │
+│ │ 3. Store in Memory Cache │ │
+│ │ │ │
+│ │ 4. Queue for Disk Save (async) │ │
+│ └───────────────────────────────────────────┘ │
+└─────────────────────┬─────────────────────────────┘
+ │
+ ▼
+┌───────────────────────────────────────────────────┐
+│ Converted Text │
+│ (Japanese Text) │
+└───────────────────────────────────────────────────┘
+```
+
+::: tip ファイルへの保存
+
+メモリキャッシュの内容は負荷にならないよう,設定した秒毎にファイルキャッシュへ自動でセーブします.
+
+:::
+
+##
diff --git a/docs/src/static/favicon.ico b/docs/src/static/favicon.ico
new file mode 100644
index 0000000..5a2affe
--- /dev/null
+++ b/docs/src/static/favicon.ico
Binary files differ
diff --git a/docs/src/static/icon.png b/docs/src/static/icon.png
new file mode 100644
index 0000000..a6865f7
--- /dev/null
+++ b/docs/src/static/icon.png
Binary files differ
diff --git a/platform-paper/build.gradle.kts b/platform-paper/build.gradle.kts
index b2caa72..41f073c 100644
--- a/platform-paper/build.gradle.kts
+++ b/platform-paper/build.gradle.kts
@@ -16,6 +16,7 @@ dependencies {
api(project(":engine"))
compileOnly("io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
+ implementation("com.charleskorn.kaml:kaml:0.102.0")
implementation("org.jetbrains.kotlin:kotlin-reflect:2.3.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
implementation("io.ktor:ktor-client-core:3.3.3")
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt
index ebec4f0..330c13b 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt
@@ -7,12 +7,14 @@ import dev.m1sk9.lunaticChat.paper.command.impl.RomajiConvertToggleCommand
import dev.m1sk9.lunaticChat.paper.command.impl.TellCommand
import dev.m1sk9.lunaticChat.paper.common.SpyPermissionManager
import dev.m1sk9.lunaticChat.paper.config.ConfigManager
+import dev.m1sk9.lunaticChat.paper.config.LunaticChatConfiguration
import dev.m1sk9.lunaticChat.paper.converter.ConversionCache
import dev.m1sk9.lunaticChat.paper.converter.GoogleIMEClient
import dev.m1sk9.lunaticChat.paper.converter.RomanjiConverter
import dev.m1sk9.lunaticChat.paper.listener.PlayerChatListener
import dev.m1sk9.lunaticChat.paper.listener.PlayerPresenceListener
import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager
+import dev.m1sk9.lunaticChat.paper.settings.YamlPlayerSettingsStorage
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import org.bukkit.event.Listener
@@ -30,90 +32,139 @@ class LunaticChat :
override fun onEnable() {
saveDefaultConfig()
- val lunaticChatConfiguration = ConfigManager.loadConfiguration(config)
+ val configuration = ConfigManager.loadConfiguration(config)
- if (lunaticChatConfiguration.debug) {
+ if (configuration.debug) {
logger.warning("LunaticChat is running in debug mode.")
- logger.info("Debug: $lunaticChatConfiguration")
+ logger.info("Debug: $configuration")
}
- if (lunaticChatConfiguration.features.japaneseConversion.enabled) {
- val settingsDir = dataFolder.resolve(lunaticChatConfiguration.features.japaneseConversion.settingsDirectory).toPath()
- playerSettingsManager =
- PlayerSettingsManager(
- settingsDirectory = settingsDir,
- plugin = this,
- logger = logger,
- )
- playerSettingsManager!!.initializeDirectory()
-
- val cache =
- ConversionCache(
- cacheFile = dataFolder.resolve(lunaticChatConfiguration.features.japaneseConversion.cacheFilePath).toPath(),
- maxEntries = lunaticChatConfiguration.features.japaneseConversion.cacheMaxEntries,
- plugin = this,
- logger = logger,
- )
- cache.loadFromDisk()
-
- val httpClient = HttpClient(CIO)
- val apiClient =
- GoogleIMEClient(
- timeout = lunaticChatConfiguration.features.japaneseConversion.apiTimeout.milliseconds,
- httpClient = httpClient,
- )
- romajiConverter =
- RomanjiConverter(
- cache = cache,
- apiClient = apiClient,
- logger = logger,
- debugMode = lunaticChatConfiguration.debug,
- )
-
- val saveInterval = lunaticChatConfiguration.features.japaneseConversion.cacheSaveIntervalSeconds * 20L
- server.scheduler.runTaskTimerAsynchronously(
- this,
- Runnable {
- cache.saveToDisk()
- },
- saveInterval,
- saveInterval,
- )
-
- server.pluginManager.registerEvents(PlayerChatListener(romajiConverter!!, playerSettingsManager!!), this)
- logger.info("Japanese conversion feature enabled.")
+ // Initialize features
+ if (configuration.features.japaneseConversion.enabled) {
+ initializeJapaneseConversionFeature(configuration)
}
+ // Initialize handlers
directMessageHandler =
DirectMessageHandler(
settingsManager = playerSettingsManager,
romanjiConverter = romajiConverter,
)
- commandRegistry =
- CommandRegistry(this)
- .registerAll(
- TellCommand(this, directMessageHandler),
- )
- if (lunaticChatConfiguration.features.quickRepliesEnabled.enabled) {
+ // Register commands and listeners
+ registerCommands(configuration)
+ registerEventListeners()
+
+ logger.info("LunaticChat enabled.")
+ }
+
+ override fun onDisable() {
+ playerSettingsManager?.saveToDisk()
+ logger.info("LunaticChat disabled.")
+ }
+
+ /**
+ * Initializes the Japanese conversion feature including:
+ * - Player settings management (YAML-based)
+ * - Conversion cache
+ * - Google IME API client
+ * - Romanji converter
+ * - Periodic cache saving task
+ */
+ private fun initializeJapaneseConversionFeature(configuration: LunaticChatConfiguration) {
+ // Initialize player settings
+ val settingsFile = dataFolder.resolve(configuration.userSettingsFilePath).toPath()
+ val storage =
+ YamlPlayerSettingsStorage(
+ settingsFile = settingsFile,
+ plugin = this,
+ logger = logger,
+ )
+
+ playerSettingsManager =
+ PlayerSettingsManager(
+ storage = storage,
+ logger = logger,
+ )
+ playerSettingsManager!!.initialize()
+
+ // Initialize conversion cache
+ val cache =
+ ConversionCache(
+ cacheFile = dataFolder.resolve(configuration.features.japaneseConversion.cacheFilePath).toPath(),
+ maxEntries = configuration.features.japaneseConversion.cacheMaxEntries,
+ plugin = this,
+ logger = logger,
+ )
+ cache.loadFromDisk()
+
+ // Initialize Google IME API client
+ val httpClient = HttpClient(CIO)
+ val apiClient =
+ GoogleIMEClient(
+ timeout = configuration.features.japaneseConversion.apiTimeout.milliseconds,
+ httpClient = httpClient,
+ )
+
+ // Initialize Romanji converter
+ romajiConverter =
+ RomanjiConverter(
+ cache = cache,
+ apiClient = apiClient,
+ logger = logger,
+ debugMode = configuration.debug,
+ )
+
+ // Schedule periodic cache saving
+ val saveInterval = configuration.features.japaneseConversion.cacheSaveIntervalSeconds * 20L
+ server.scheduler.runTaskTimerAsynchronously(
+ this,
+ Runnable {
+ cache.saveToDisk()
+ },
+ saveInterval,
+ saveInterval,
+ )
+
+ // Register event listener for Japanese conversion
+ server.pluginManager.registerEvents(PlayerChatListener(romajiConverter!!, playerSettingsManager!!), this)
+
+ logger.info("Japanese conversion feature enabled.")
+ }
+
+ /**
+ * Registers all commands based on enabled features.
+ */
+ private fun registerCommands(configuration: LunaticChatConfiguration) {
+ commandRegistry = CommandRegistry(this)
+
+ // Always register /tell command
+ commandRegistry.registerAll(
+ TellCommand(this, directMessageHandler),
+ )
+
+ // Register /reply command if quick replies are enabled
+ if (configuration.features.quickRepliesEnabled.enabled) {
commandRegistry.registerAll(
ReplyCommand(this, directMessageHandler),
)
}
- if (lunaticChatConfiguration.features.japaneseConversion.enabled) {
+
+ // Register /jp command if Japanese conversion is enabled
+ if (configuration.features.japaneseConversion.enabled) {
commandRegistry.registerAll(
RomajiConvertToggleCommand(this, playerSettingsManager!!),
)
}
- commandRegistry.initialize()
-
- server.pluginManager.registerEvents(SpyPermissionManager, this)
- server.pluginManager.registerEvents(PlayerPresenceListener(this, playerSettingsManager), this)
- logger.info("LunaticChat enabled.")
+ commandRegistry.initialize()
}
- override fun onDisable() {
- logger.info("LunaticChat disabled.")
+ /**
+ * Registers all event listeners.
+ */
+ private fun registerEventListeners() {
+ server.pluginManager.registerEvents(SpyPermissionManager, this)
+ server.pluginManager.registerEvents(PlayerPresenceListener(this), this)
}
}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt
index 64e35b2..8580504 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt
@@ -42,11 +42,6 @@ object ConfigManager {
3000,
),
apiRetryAttempts = configFile.getInt("features.japaneseConversion.api.retryAttempts", 2),
- settingsDirectory =
- configFile.getString(
- "features.japaneseConversion.settings.directory",
- "settings",
- )!!,
),
),
messageFormat =
@@ -58,6 +53,11 @@ object ConfigManager {
)!!,
),
debug = configFile.getBoolean("debug", false),
+ userSettingsFilePath =
+ configFile.getString(
+ "userSettingsFilePath",
+ "player-settings.yaml",
+ )!!,
)
lunaticChatConfiguration = loadedConfig
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/LunaticChatConfiguration.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/LunaticChatConfiguration.kt
index 2475086..e94687f 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/LunaticChatConfiguration.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/LunaticChatConfiguration.kt
@@ -7,4 +7,5 @@ data class LunaticChatConfiguration(
val features: FeaturesConfig,
val messageFormat: MessageFormatConfig,
val debug: Boolean = false,
+ val userSettingsFilePath: String = "player-settings.yaml",
)
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/JapaneseConversionFeatureConfig.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/JapaneseConversionFeatureConfig.kt
index df72f1d..c329085 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/JapaneseConversionFeatureConfig.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/JapaneseConversionFeatureConfig.kt
@@ -7,5 +7,4 @@ data class JapaneseConversionFeatureConfig(
val cacheFilePath: String,
val apiTimeout: Long,
val apiRetryAttempts: Int,
- val settingsDirectory: String = "settings",
)
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerPresenceListener.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerPresenceListener.kt
index 579f50a..623a544 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerPresenceListener.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerPresenceListener.kt
@@ -1,24 +1,16 @@
package dev.m1sk9.lunaticChat.paper.listener
import dev.m1sk9.lunaticChat.paper.LunaticChat
-import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
-import org.bukkit.event.player.PlayerJoinEvent
import org.bukkit.event.player.PlayerQuitEvent
class PlayerPresenceListener(
private val lunaticChat: LunaticChat,
- private val settingsManager: PlayerSettingsManager?,
) : Listener {
@EventHandler(ignoreCancelled = true)
- fun onJoin(event: PlayerJoinEvent) {
- settingsManager?.loadPlayerSettings(event.player.uniqueId)
- }
-
- @EventHandler(ignoreCancelled = true)
fun onQuit(event: PlayerQuitEvent) {
lunaticChat.directMessageHandler.clearPlayer(event.player)
- settingsManager?.unloadPlayerSettings(event.player.uniqueId)
+ // Settings remain in memory, no unloading needed
}
}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsData.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsData.kt
new file mode 100644
index 0000000..1881f0c
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsData.kt
@@ -0,0 +1,29 @@
+package dev.m1sk9.lunaticChat.paper.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/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManager.kt
index 634036f..14a20a4 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManager.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManager.kt
@@ -1,87 +1,44 @@
package dev.m1sk9.lunaticChat.paper.settings
-import kotlinx.serialization.json.Json
-import org.bukkit.Bukkit
-import org.bukkit.plugin.java.JavaPlugin
-import java.nio.file.Path
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
-import java.util.concurrent.atomic.AtomicBoolean
import java.util.logging.Logger
-import kotlin.io.path.bufferedReader
-import kotlin.io.path.createDirectories
-import kotlin.io.path.exists
-import kotlin.io.path.writeText
/**
- * Manages per-player chat settings persistence.
- * Provides in-memory cache with async disk I/O for player settings.
+ * Manages player chat settings with YAML-based persistence.
+ * All settings are loaded into memory at startup and saved asynchronously on changes.
*
- * @property settingsDirectory The directory where player settings files are stored
- * @property plugin The plugin instance for scheduling async tasks
+ * @property storage The YAML storage layer
* @property logger The logger for logging operations
*/
class PlayerSettingsManager(
- private val settingsDirectory: Path,
- private val plugin: JavaPlugin,
+ private val storage: YamlPlayerSettingsStorage,
private val logger: Logger,
) {
- private val settingsCache = ConcurrentHashMap<UUID, PlayerChatSettings>()
- private val saveQueue = ConcurrentHashMap<UUID, AtomicBoolean>()
+ private val japaneseConversionCache = ConcurrentHashMap<UUID, Boolean>()
+ private lateinit var settingsData: PlayerSettingsData
/**
- * Initializes the settings directory if it doesn't exist.
+ * Initializes the settings manager by loading all settings from disk into memory.
+ * This should be called once during plugin startup.
*/
- fun initializeDirectory() {
- if (!settingsDirectory.exists()) {
- settingsDirectory.createDirectories()
- logger.info("Created player settings directory at: $settingsDirectory")
- }
+ fun initialize() {
+ settingsData = storage.loadFromDisk()
+ japaneseConversionCache.putAll(settingsData.japaneseConversion)
+ logger.info("Loaded settings for ${japaneseConversionCache.size} players")
}
/**
- * Loads player settings from disk into memory.
- * If the settings file doesn't exist, creates default settings.
- *
- * @param uuid The UUID of the player
- * @return The loaded or newly created player settings
- */
- fun loadPlayerSettings(uuid: UUID): PlayerChatSettings {
- val settingsFile = settingsDirectory.resolve("$uuid.json")
-
- if (!settingsFile.exists()) {
- logger.fine("Settings file not found for player $uuid, creating default settings.")
- val defaultSettings = PlayerChatSettings(uuid = uuid, japaneseConversionEnabled = false)
- settingsCache[uuid] = defaultSettings
- queueSaveToDisk(uuid)
- return defaultSettings
- }
-
- try {
- val jsonBuffer = settingsFile.bufferedReader().use { it.readText() }
- val settings = Json.decodeFromString<PlayerChatSettings>(jsonBuffer)
- settingsCache[uuid] = settings
- logger.fine("Loaded settings for player $uuid from disk.")
- return settings
- } catch (e: Exception) {
- logger.warning("Failed to load settings for player $uuid: ${e.message}. Using default settings.")
- val defaultSettings = PlayerChatSettings(uuid = uuid, japaneseConversionEnabled = false)
- settingsCache[uuid] = defaultSettings
- return defaultSettings
- }
- }
-
- /**
- * Retrieves settings from cache.
- * If settings are not cached, creates and caches default settings.
+ * Retrieves settings for a player.
+ * If settings don't exist, returns default settings.
*
* @param uuid The UUID of the player
* @return The player's settings
*/
- fun getSettings(uuid: UUID): PlayerChatSettings =
- settingsCache.getOrPut(uuid) {
- PlayerChatSettings(uuid = uuid, japaneseConversionEnabled = false)
- }
+ fun getSettings(uuid: UUID): PlayerChatSettings {
+ val enabled = japaneseConversionCache.getOrDefault(uuid, false)
+ return PlayerChatSettings(uuid = uuid, japaneseConversionEnabled = enabled)
+ }
/**
* Updates player settings in cache and queues async save to disk.
@@ -89,60 +46,22 @@ class PlayerSettingsManager(
* @param settings The updated settings to save
*/
fun updateSettings(settings: PlayerChatSettings) {
- settingsCache[settings.uuid] = settings
- queueSaveToDisk(settings.uuid)
- logger.fine("Updated settings for player ${settings.uuid}")
- }
+ japaneseConversionCache[settings.uuid] = settings.japaneseConversionEnabled
- /**
- * Removes player settings from cache.
- * Called when a player quits. Settings remain persisted on disk.
- *
- * @param uuid The UUID of the player
- */
- fun unloadPlayerSettings(uuid: UUID) {
- settingsCache.remove(uuid)
- saveQueue.remove(uuid)
- logger.fine("Unloaded settings for player $uuid from cache.")
- }
-
- /**
- * Saves player settings from memory to disk immediately.
- * This operation is synchronous and should only be called from async context.
- *
- * @param uuid The UUID of the player
- */
- private fun savePlayerSettings(uuid: UUID) {
- val settings = settingsCache[uuid] ?: return
+ settingsData =
+ settingsData.copy(
+ japaneseConversion = japaneseConversionCache.toMap(),
+ )
- try {
- val settingsFile = settingsDirectory.resolve("$uuid.json")
- val jsonBuffer = Json.encodeToString(PlayerChatSettings.serializer(), settings)
- settingsFile.writeText(jsonBuffer)
- logger.fine("Saved settings for player $uuid to disk.")
- } catch (e: Exception) {
- logger.severe("Failed to save settings for player $uuid: ${e.message}")
- }
+ storage.queueAsyncSave(settingsData)
+ logger.fine("Updated settings for player ${settings.uuid}")
}
/**
- * Queues an async save operation for a player's settings.
- * Multiple save requests within 5 seconds are batched into a single save operation.
- *
- * @param uuid The UUID of the player
+ * Forces an immediate synchronous save of all settings to disk.
+ * This should only be called during plugin shutdown.
*/
- private fun queueSaveToDisk(uuid: UUID) {
- val queueFlag = saveQueue.getOrPut(uuid) { AtomicBoolean(false) }
-
- if (queueFlag.compareAndSet(false, true)) {
- Bukkit.getScheduler().runTaskAsynchronously(
- plugin,
- Runnable {
- Thread.sleep(5000) // 5 seconds delay to batch multiple save requests
- queueFlag.set(false)
- savePlayerSettings(uuid)
- },
- )
- }
+ fun saveToDisk() {
+ storage.saveToDisk(settingsData)
}
}
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/UUIDASStringSerializer.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/UUIDASStringSerializer.kt
new file mode 100644
index 0000000..04e688b
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/UUIDASStringSerializer.kt
@@ -0,0 +1,27 @@
+package dev.m1sk9.lunaticChat.paper.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/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt
new file mode 100644
index 0000000..ed90f0c
--- /dev/null
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/YamlPlayerSettingsStorage.kt
@@ -0,0 +1,114 @@
+package dev.m1sk9.lunaticChat.paper.settings
+
+import com.charleskorn.kaml.Yaml
+import org.bukkit.Bukkit
+import org.bukkit.plugin.java.JavaPlugin
+import java.nio.file.Path
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.logging.Logger
+import kotlin.io.path.bufferedReader
+import kotlin.io.path.exists
+import kotlin.io.path.listDirectoryEntries
+import kotlin.io.path.writeText
+
+/**
+ * Handles YAML file I/O operations for player settings.
+ * Provides backup functionality and async save with debouncing.
+ *
+ * @property settingsFile The path to the YAML settings file
+ * @property plugin The plugin instance for scheduling async tasks
+ * @property logger The logger for logging operations
+ */
+class YamlPlayerSettingsStorage(
+ private val settingsFile: Path,
+ private val plugin: JavaPlugin,
+ private val logger: Logger,
+) {
+ private val yaml = Yaml.default
+ private val saveFlag = AtomicBoolean(false)
+
+ /**
+ * Loads player settings from the YAML file.
+ * If the file doesn't exist, returns empty settings.
+ * If loading fails, attempts to restore from backup.
+ *
+ * @return The loaded settings or empty settings if file doesn't exist
+ */
+ fun loadFromDisk(): PlayerSettingsData {
+ if (!settingsFile.exists()) {
+ logger.info("Settings file not found, will create on first save")
+ return PlayerSettingsData()
+ }
+
+ return try {
+ val yamlContent = settingsFile.bufferedReader().use { it.readText() }
+ yaml.decodeFromString(PlayerSettingsData.serializer(), yamlContent)
+ } catch (e: Exception) {
+ logger.severe("Failed to load settings file: ${e.message}")
+
+ val backup = findLatestBackup()
+ if (backup != null) {
+ logger.warning("Attempting to restore from backup: $backup")
+ try {
+ val content = backup.bufferedReader().use { it.readText() }
+ return yaml.decodeFromString(PlayerSettingsData.serializer(), content)
+ } catch (backupError: Exception) {
+ logger.severe("Backup restoration failed: ${backupError.message}")
+ }
+ }
+
+ logger.warning("Using empty settings as fallback")
+ PlayerSettingsData()
+ }
+ }
+
+ /**
+ * Saves player settings to the YAML file synchronously.
+ * This should only be called from async context or during shutdown.
+ *
+ * @param data The settings data to save
+ */
+ fun saveToDisk(data: PlayerSettingsData) {
+ try {
+ val yamlContent = yaml.encodeToString(PlayerSettingsData.serializer(), data)
+ settingsFile.writeText(yamlContent)
+ logger.fine("Saved player settings to disk")
+ } catch (e: Exception) {
+ logger.severe("Failed to save settings: ${e.message}")
+ }
+ }
+
+ /**
+ * Queues an async save operation with 5-second debouncing.
+ * Multiple save requests within 5 seconds are batched into a single save.
+ *
+ * @param data The settings data to save
+ */
+ fun queueAsyncSave(data: PlayerSettingsData) {
+ if (saveFlag.compareAndSet(false, true)) {
+ Bukkit.getScheduler().runTaskAsynchronously(
+ plugin,
+ Runnable {
+ Thread.sleep(5000) // 5 seconds delay to batch multiple save requests
+ saveFlag.set(false)
+ saveToDisk(data)
+ },
+ )
+ }
+ }
+
+ /**
+ * Finds the most recent backup file.
+ *
+ * @return The path to the latest backup, or null if no backups exist
+ */
+ private fun findLatestBackup(): Path? =
+ try {
+ settingsFile.parent
+ .listDirectoryEntries("player-settings.yaml.backup.*")
+ .maxByOrNull { it.fileName.toString() }
+ } catch (e: Exception) {
+ logger.warning("Failed to find backup: ${e.message}")
+ null
+ }
+}
diff --git a/platform-paper/src/main/resources/config.yml b/platform-paper/src/main/resources/config.yml
index f8b8d83..f1ebe64 100644
--- a/platform-paper/src/main/resources/config.yml
+++ b/platform-paper/src/main/resources/config.yml
@@ -15,6 +15,9 @@
# If enabled, Activate LunaticChat's debug mode, which provides detailed logging for troubleshooting.
debug: false
+# Path to the YAML file storing player settings
+userSettingsFilePath: "player-settings.yaml"
+
# ----------------------------------------------
# ----------- Features Settings ------------
# ----------------------------------------------
@@ -27,14 +30,17 @@ features:
# If enabled, enables the conversion function from Roman letters to hiragana.
enabled: false
cache:
+ # Specifies the maximum number of entries to store in the Romanization conversion cache.
maxEntries: 500
+ # Specify the interval (in seconds) for saving the Romanization conversion cache to disk.
saveIntervalSeconds: 300
+ # Specify the file path where the cache for Romanization conversion is saved.
filePath: "conversion_cache.json"
api:
+ # Specify the timeout duration (in milliseconds) for API requests to the Romanization conversion service.
timeout: 3000
+ # Specify the number of retry attempts for failed API requests to the Romanization conversion service.
retryAttempts: 2
- settings:
- directory: "settings"
# ----------------------------------------------
# --------- Message Format Settings --------