summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-08-02 19:42:01 +0900
committerSho Sakuma <me@m1sk9.dev>2026-08-02 19:42:01 +0900
commitbc8010bc1b5401f11c80b565ba9a4de7d969a0a9 (patch)
tree0841b86822ad299ae5523a62d4919a90c913677c
parent7e7d0875087185e6c68bb29f1d60bd51b300bfef (diff)
downloadLunaticChat-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>
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt26
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceContainer.kt6
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt77
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/settings/PlayerSettingsManager.kt65
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 },
+ )
}