diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-01-11 18:50:12 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-11 18:50:12 +0900 |
| commit | efdaff7325f6ae7abec46b64d5b4b6019c01f5f2 (patch) | |
| tree | 3d7854a0b6ca9800e475cc9577149475875c57ae | |
| parent | ae0796559160430b5d251543ec27184467f32d8e (diff) | |
| parent | a7c47276162f40e7e3fa673c4afbc74e52d8c6ee (diff) | |
| download | LunaticChat-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
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 Binary files differnew file mode 100644 index 0000000..5a2affe --- /dev/null +++ b/docs/src/static/favicon.ico diff --git a/docs/src/static/icon.png b/docs/src/static/icon.png Binary files differnew file mode 100644 index 0000000..a6865f7 --- /dev/null +++ b/docs/src/static/icon.png 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 -------- |
