diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-08-02 19:42:01 +0900 |
|---|---|---|
| committer | Sho Sakuma <me@m1sk9.dev> | 2026-08-02 19:42:01 +0900 |
| commit | bc8010bc1b5401f11c80b565ba9a4de7d969a0a9 (patch) | |
| tree | 0841b86822ad299ae5523a62d4919a90c913677c | |
| parent | 7e7d0875087185e6c68bb29f1d60bd51b300bfef (diff) | |
| download | LunaticChat-bc8010bc1b5401f11c80b565ba9a4de7d969a0a9.tar.gz LunaticChat-bc8010bc1b5401f11c80b565ba9a4de7d969a0a9.tar.bz2 LunaticChat-bc8010bc1b5401f11c80b565ba9a4de7d969a0a9.zip | |
refactor: let each service live in exactly one place
ServiceInitializer held ten nullable fields alongside the ServiceContainer
it returns. Six of them were written and never read; the remaining four
made shutdown() ambiguous, reading conversionCache and channelMessageLogger
from its own fields but everything else from the container it was handed.
LunaticChat then mirrored seven more into public vars, one of which
(channelMessageHandler) nothing read at all.
Now the container is the single place a service lives: the initializer
builds and returns, shutdown and the periodic task read from what they are
given, and the plugin's public properties delegate rather than copy. A new
service is one field instead of three, and no copy can go stale.
PlayerSettingsManager had the same shape at a smaller scale: three UUID
maps plus a PlayerSettingsData kept in sync by hand, where the data object
was a pure derivation rebuilt - three full map copies - on every toggle. It
now keeps one map and derives the snapshot at save time, which also closes
the window where queueAsyncSave captured state that changes before the
debounce fires.
Co-Authored-By: Claude <noreply@anthropic.com>
4 files changed, 77 insertions, 97 deletions
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 f0bbc2c..5b8cab1 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 @@ -2,7 +2,6 @@ package dev.m1sk9.lunaticChat.paper import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager -import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelMessageHandler import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelNotificationHandler import dev.m1sk9.lunaticChat.paper.chat.handler.DirectMessageHandler import dev.m1sk9.lunaticChat.paper.command.core.CommandRegistry @@ -30,13 +29,13 @@ import java.util.concurrent.atomic.AtomicBoolean class LunaticChat : JavaPlugin(), Listener { - lateinit var directMessageHandler: DirectMessageHandler - lateinit var languageManager: LanguageManager - var channelManager: ChannelManager? = null - var channelMembershipManager: ChannelMembershipManager? = null - var channelMessageHandler: ChannelMessageHandler? = null - var channelNotificationHandler: ChannelNotificationHandler? = null - var velocityConnectionManager: VelocityConnectionManager? = null + // Read by commands, which reach the plugin instance but not the container. + val directMessageHandler: DirectMessageHandler get() = services.directMessageHandler + val languageManager: LanguageManager get() = services.languageManager + val channelManager: ChannelManager? get() = services.channelManager + val channelMembershipManager: ChannelMembershipManager? get() = services.channelMembershipManager + val channelNotificationHandler: ChannelNotificationHandler? get() = services.channelNotificationHandler + val velocityConnectionManager: VelocityConnectionManager? get() = services.velocityConnectionManager private lateinit var services: ServiceContainer private lateinit var configuration: LunaticChatConfiguration @@ -71,17 +70,8 @@ class LunaticChat : ) services = serviceInitializer.initialize() - // Set public API properties (for command access) - directMessageHandler = services.directMessageHandler - languageManager = services.languageManager - channelManager = services.channelManager - channelMembershipManager = services.channelMembershipManager - channelMessageHandler = services.channelMessageHandler - channelNotificationHandler = services.channelNotificationHandler - velocityConnectionManager = services.velocityConnectionManager - // Schedule periodic tasks - serviceInitializer.schedulePeriodicTasks() + serviceInitializer.schedulePeriodicTasks(services) // Register commands and listeners registerCommands() diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceContainer.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceContainer.kt index 4d668f2..48f6e37 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceContainer.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceContainer.kt @@ -2,9 +2,11 @@ package dev.m1sk9.lunaticChat.paper import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager +import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMessageLogger import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelMessageHandler import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelNotificationHandler import dev.m1sk9.lunaticChat.paper.chat.handler.DirectMessageHandler +import dev.m1sk9.lunaticChat.paper.converter.ConversionCache import dev.m1sk9.lunaticChat.paper.converter.RomanjiConverter import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager @@ -23,8 +25,10 @@ import dev.m1sk9.lunaticChat.paper.velocity.VelocityConnectionManager * @property playerSettingsManager Always available (required for DM notifications) * @property directMessageHandler Always available (core feature) * @property romajiConverter Optional (only when Japanese conversion feature is enabled) + * @property conversionCache Optional (only when Japanese conversion feature is enabled) * @property channelManager Optional (only when channel chat feature is enabled) * @property channelMembershipManager Optional (only when channel chat feature is enabled) + * @property channelMessageLogger Optional (only when channel message logging is enabled) * @property channelMessageHandler Optional (only when channel chat feature is enabled) * @property channelNotificationHandler Optional (only when channel chat feature is enabled) * @property velocityConnectionManager Optional (only when Velocity integration is enabled) @@ -37,8 +41,10 @@ data class ServiceContainer( val playerSettingsManager: PlayerSettingsManager, val directMessageHandler: DirectMessageHandler, val romajiConverter: RomanjiConverter? = null, + val conversionCache: ConversionCache? = null, val channelManager: ChannelManager? = null, val channelMembershipManager: ChannelMembershipManager? = null, + val channelMessageLogger: ChannelMessageLogger? = null, val channelMessageHandler: ChannelMessageHandler? = null, val channelNotificationHandler: ChannelNotificationHandler? = null, val velocityConnectionManager: VelocityConnectionManager? = null, diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt index 6213348..c118c12 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt @@ -36,6 +36,7 @@ private data class ChannelComponents( val channelMembershipManager: ChannelMembershipManager, val channelMessageHandler: ChannelMessageHandler, val channelNotificationHandler: ChannelNotificationHandler, + val channelMessageLogger: ChannelMessageLogger?, ) /** @@ -50,16 +51,6 @@ class ServiceInitializer( private val httpClient: HttpClient, private val logger: Logger, ) { - private var conversionCache: ConversionCache? = null - private var channelManager: ChannelManager? = null - private var channelMembershipManager: ChannelMembershipManager? = null - private var channelMessageHandler: ChannelMessageHandler? = null - private var channelNotificationHandler: ChannelNotificationHandler? = null - private var channelMessageLogger: ChannelMessageLogger? = null - private var velocityConnectionManager: VelocityConnectionManager? = null - private var crossServerChatManager: CrossServerChatManager? = null - private var crossServerDirectMessageManager: CrossServerDirectMessageManager? = null - private var remotePlayerRegistry: RemotePlayerRegistry? = null private val handshakeCompleted = AtomicBoolean(false) /** @@ -89,12 +80,13 @@ class ServiceInitializer( val playerSettingsManager = initializePlayerSettingsManager() // 3. Initialize Japanese conversion (optional) - val romajiConverter = + val japaneseConversion = if (configuration.features.japaneseConversion.enabled) { initializeJapaneseConversion() } else { null } + val romajiConverter = japaneseConversion?.first // 4. Initialize channel manager, membership manager, channel message handler, and notification handler val channelComponents = @@ -103,11 +95,6 @@ class ServiceInitializer( } else { null } - val channelManager = channelComponents?.channelManager - val channelMembershipManager = channelComponents?.channelMembershipManager - val channelMessageHandler = channelComponents?.channelMessageHandler - val channelNotificationHandler = channelComponents?.channelNotificationHandler - // 5. Initialize handlers val directMessageHandler = DirectMessageHandler( @@ -137,26 +124,31 @@ class ServiceInitializer( } // 8. Initialize cross-server direct message manager and presence registry (optional) - if (configuration.features.velocityIntegration.enabled && - configuration.features.velocityIntegration.crossServerDirectMessage && - velocityManager != null - ) { - initializeCrossServerDirectMessage(velocityManager, directMessageHandler, languageManager) - } + val crossServerDirectMessage = + if (configuration.features.velocityIntegration.enabled && + configuration.features.velocityIntegration.crossServerDirectMessage && + velocityManager != null + ) { + initializeCrossServerDirectMessage(velocityManager, directMessageHandler, languageManager) + } else { + null + } return ServiceContainer( languageManager = languageManager, playerSettingsManager = playerSettingsManager, directMessageHandler = directMessageHandler, romajiConverter = romajiConverter, - channelManager = channelManager, - channelMembershipManager = channelMembershipManager, - channelMessageHandler = channelMessageHandler, - channelNotificationHandler = channelNotificationHandler, + conversionCache = japaneseConversion?.second, + channelManager = channelComponents?.channelManager, + channelMembershipManager = channelComponents?.channelMembershipManager, + channelMessageLogger = channelComponents?.channelMessageLogger, + channelMessageHandler = channelComponents?.channelMessageHandler, + channelNotificationHandler = channelComponents?.channelNotificationHandler, velocityConnectionManager = velocityManager, crossServerChatManager = crossServerManager, - crossServerDirectMessageManager = crossServerDirectMessageManager, - remotePlayerRegistry = remotePlayerRegistry, + crossServerDirectMessageManager = crossServerDirectMessage?.first, + remotePlayerRegistry = crossServerDirectMessage?.second, ) } @@ -188,7 +180,7 @@ class ServiceInitializer( * - Google IME API client * - Romanji converter */ - private fun initializeJapaneseConversion(): RomanjiConverter { + private fun initializeJapaneseConversion(): Pair<RomanjiConverter, ConversionCache> { // Initialize conversion cache val cache = ConversionCache( @@ -198,7 +190,6 @@ class ServiceInitializer( logger = logger, ) cache.loadFromDisk() - conversionCache = cache // Initialize Google IME API client val apiClient = @@ -217,7 +208,7 @@ class ServiceInitializer( ) logger.info("Japanese conversion feature enabled.") - return converter + return converter to cache } /** @@ -242,7 +233,6 @@ class ServiceInitializer( config = configuration.features.channelChat, ) manager.initialize() - channelManager = manager val membershipManager = ChannelMembershipManager( @@ -250,7 +240,6 @@ class ServiceInitializer( logger = logger, config = configuration.features.channelChat, ) - channelMembershipManager = membershipManager // Initialize channel message logger if enabled val messageLogger = @@ -265,7 +254,6 @@ class ServiceInitializer( maxFileSizeBytes = configuration.features.channelChat.messageLogging.maxFileSizeMB * 1024L * 1024L, retentionDays = configuration.features.channelChat.messageLogging.retentionDays, ).also { - channelMessageLogger = it logger.info( "Channel message logging enabled (retention: ${configuration.features.channelChat.messageLogging.retentionDays} days)", ) @@ -285,14 +273,12 @@ class ServiceInitializer( io.ktor.util.logging .KtorSimpleLogger("ChannelMessageHandler"), ) - channelMessageHandler = messageHandler val notificationHandler = ChannelNotificationHandler( channelManager = manager, languageManager = languageManager, ) - channelNotificationHandler = notificationHandler logger.info( "Channel manager, membership manager, " + @@ -303,6 +289,7 @@ class ServiceInitializer( channelMembershipManager = membershipManager, channelMessageHandler = messageHandler, channelNotificationHandler = notificationHandler, + channelMessageLogger = messageLogger, ) } @@ -318,7 +305,6 @@ class ServiceInitializer( logger = logger, ) manager.initialize() - velocityConnectionManager = manager // Register listener for first player join plugin.server.pluginManager.registerEvents( @@ -360,7 +346,6 @@ class ServiceInitializer( configuration = configuration, cacheSize = configuration.features.velocityIntegration.messageDeduplicationCacheSize, ) - crossServerChatManager = manager // Set the manager in VelocityConnectionManager to handle incoming messages velocityManager.setCrossServerChatManager(manager) @@ -380,9 +365,8 @@ class ServiceInitializer( velocityManager: VelocityConnectionManager, directMessageHandler: DirectMessageHandler, languageManager: LanguageManager, - ) { + ): Pair<CrossServerDirectMessageManager, RemotePlayerRegistry> { val registry = RemotePlayerRegistry(configuration.features.velocityIntegration.serverName) - remotePlayerRegistry = registry directMessageHandler.remotePlayerRegistry = registry val manager = @@ -394,11 +378,11 @@ class ServiceInitializer( languageManager = languageManager, cacheSize = configuration.features.velocityIntegration.messageDeduplicationCacheSize, ) - crossServerDirectMessageManager = manager velocityManager.setCrossServerDirectMessageManager(manager, registry) logger.info("Cross-server direct messages initialized") + return manager to registry } /** @@ -432,15 +416,16 @@ class ServiceInitializer( * Schedules periodic tasks such as cache saving. * Uses Folia-compatible AsyncScheduler API. */ - fun schedulePeriodicTasks() { - if (configuration.features.japaneseConversion.enabled && conversionCache != null) { + fun schedulePeriodicTasks(services: ServiceContainer) { + val conversionCache = services.conversionCache + if (conversionCache != null) { val intervalSeconds = configuration.features.japaneseConversion .cacheSaveIntervalSeconds .toLong() plugin.server.asyncScheduler.runAtFixedRate( plugin, - { conversionCache?.saveToDisk() }, + { conversionCache.saveToDisk() }, intervalSeconds, intervalSeconds, TimeUnit.SECONDS, @@ -453,9 +438,9 @@ class ServiceInitializer( */ fun shutdown(services: ServiceContainer) { services.playerSettingsManager.saveToDisk() - conversionCache?.saveToDisk() + services.conversionCache?.saveToDisk() services.channelManager?.saveToDisk() - channelMessageLogger?.shutdown() + services.channelMessageLogger?.shutdown() services.velocityConnectionManager?.shutdown() } } 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 f0927d8..de35c55 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 @@ -17,21 +17,32 @@ class PlayerSettingsManager( private val storage: YamlPlayerSettingsStorage, private val logger: Logger, ) { - private val japaneseConversionCache = ConcurrentHashMap<UUID, Boolean>() - private val directMessageNotificationCache = ConcurrentHashMap<UUID, Boolean>() - private val channelMessageNotificationCache = ConcurrentHashMap<UUID, Boolean>() - private lateinit var settingsData: PlayerSettingsData + private val settings = ConcurrentHashMap<UUID, PlayerChatSettings>() + + // Written back unchanged: nothing migrates on it yet, but rewriting the file must not + // silently relabel a schema this build does not understand. + private var schemaVersion = PlayerSettingsData().version /** * Initializes the settings manager by loading all settings from disk into memory. * This should be called once during plugin startup. */ fun initialize() { - settingsData = storage.loadFromDisk() - japaneseConversionCache.putAll(settingsData.japaneseConversion) - directMessageNotificationCache.putAll(settingsData.directMessageNotification) - channelMessageNotificationCache.putAll(settingsData.channelMessageNotification) - logger.info("Loaded settings for ${japaneseConversionCache.size} players") + val data = storage.loadFromDisk() + schemaVersion = data.version + val knownPlayers = + data.japaneseConversion.keys + data.directMessageNotification.keys + data.channelMessageNotification.keys + + knownPlayers.forEach { uuid -> + settings[uuid] = + PlayerChatSettings( + uuid = uuid, + japaneseConversionEnabled = data.japaneseConversion.getOrDefault(uuid, true), + directMessageNotificationEnabled = data.directMessageNotification.getOrDefault(uuid, true), + channelMessageNotificationEnabled = data.channelMessageNotification.getOrDefault(uuid, true), + ) + } + logger.info("Loaded settings for ${settings.size} players") } /** @@ -41,17 +52,7 @@ class PlayerSettingsManager( * @param uuid The UUID of the player * @return The player's settings */ - fun getSettings(uuid: UUID): PlayerChatSettings { - val japaneseConversionEnabled = japaneseConversionCache.getOrDefault(uuid, true) - val directMessageNotificationEnabled = directMessageNotificationCache.getOrDefault(uuid, true) - val channelMessageNotificationEnabled = channelMessageNotificationCache.getOrDefault(uuid, true) - return PlayerChatSettings( - uuid = uuid, - japaneseConversionEnabled = japaneseConversionEnabled, - directMessageNotificationEnabled = directMessageNotificationEnabled, - channelMessageNotificationEnabled = channelMessageNotificationEnabled, - ) - } + fun getSettings(uuid: UUID): PlayerChatSettings = settings[uuid] ?: PlayerChatSettings(uuid = uuid) /** * Updates player settings in cache and queues async save to disk. @@ -59,18 +60,8 @@ class PlayerSettingsManager( * @param settings The updated settings to save */ fun updateSettings(settings: PlayerChatSettings) { - japaneseConversionCache[settings.uuid] = settings.japaneseConversionEnabled - directMessageNotificationCache[settings.uuid] = settings.directMessageNotificationEnabled - channelMessageNotificationCache[settings.uuid] = settings.channelMessageNotificationEnabled - - settingsData = - settingsData.copy( - japaneseConversion = japaneseConversionCache.toMap(), - directMessageNotification = directMessageNotificationCache.toMap(), - channelMessageNotification = channelMessageNotificationCache.toMap(), - ) - - storage.queueAsyncSave(settingsData) + this.settings[settings.uuid] = settings + storage.queueAsyncSave(snapshot()) logger.fine("Updated settings for player ${settings.uuid}") } @@ -79,6 +70,14 @@ class PlayerSettingsManager( * This should only be called during plugin shutdown. */ fun saveToDisk() { - storage.saveToDisk(settingsData) + storage.saveToDisk(snapshot()) } + + private fun snapshot(): PlayerSettingsData = + PlayerSettingsData( + version = schemaVersion, + japaneseConversion = settings.mapValues { it.value.japaneseConversionEnabled }, + directMessageNotification = settings.mapValues { it.value.directMessageNotificationEnabled }, + channelMessageNotification = settings.mapValues { it.value.channelMessageNotificationEnabled }, + ) } |
