diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-08-03 00:45:44 +0900 |
|---|---|---|
| committer | Sho Sakuma <me@m1sk9.dev> | 2026-08-05 00:42:23 +0900 |
| commit | 790dcf942a3acab1268d46e3fc22be25a4ee02d5 (patch) | |
| tree | 386234a8c0fbbc37a4d606f8b6db165ab95f0444 /platform-paper | |
| parent | 1ee7a7fb0b24a99daace60016468774a5bd6fbf4 (diff) | |
| download | LunaticChat-790dcf942a3acab1268d46e3fc22be25a4ee02d5.tar.gz LunaticChat-790dcf942a3acab1268d46e3fc22be25a4ee02d5.tar.bz2 LunaticChat-790dcf942a3acab1268d46e3fc22be25a4ee02d5.zip | |
perf: only do spy and cache work when it will be used
Spy notification ran per recipient what it could run once: both handlers
looked up general.spyMessage and rebuilt the hover component inside the
forEach, and the direct message path also allocated a Set inside the
filter predicate, once per spy per message. It formatted the spy copy of
the message before discovering there were no spies to send it to - and
spies are rare, so that was the normal case.
SpyPermissionManager.notifySpies now owns the whole shape. It takes the
body as a lambda so nothing is built for an empty audience, attaches the
hover once, and reads the roster directly rather than through
getDirectMessageSpyPlayers()'s defensive copy. It also puts "who must not
see this" in one place; the two handlers had drifted to expressing it by
name in one and by UUID in the other.
ConversionCache tracked no dirtiness, so the periodic task re-serialized
and rewrote the entire cache file on its fixed interval whether or not
anyone had chatted, while every single put scheduled another full rewrite
five seconds out. It now records that it changed and the periodic task is
the only writer, returning immediately when there is nothing to write. A
failed write marks the cache dirty again so the next tick retries.
Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'platform-paper')
5 files changed, 44 insertions, 36 deletions
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 6d47788..fdfb9fa 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 @@ -186,7 +186,6 @@ class ServiceInitializer( ConversionCache( cacheFile = plugin.dataFolder.resolve(configuration.features.japaneseConversion.cacheFilePath).toPath(), maxEntries = configuration.features.japaneseConversion.cacheMaxEntries, - saver = DebouncedSaver(plugin), logger = logger, ) cache.loadFromDisk() diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt index 4cfd061..4da0c1a 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt @@ -12,7 +12,6 @@ import dev.m1sk9.lunaticChat.paper.i18n.withChatPlaceholders import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager import io.ktor.util.logging.Logger import net.kyori.adventure.text.Component -import net.kyori.adventure.text.event.HoverEvent import org.bukkit.Bukkit import org.bukkit.entity.Player @@ -44,19 +43,10 @@ class ChannelMessageHandler( // Send to spy players (exclude sender and channel members) val memberIds = context.members.map { it.playerId }.toSet() - SpyPermissionManager - .getDirectMessageSpyPlayers() - .values - .filter { it.isOnline && it.uniqueId != playerId && it.uniqueId !in memberIds } - .forEach { - it.sendMessage( - formattedMessage.hoverEvent( - HoverEvent.showText( - Component.text(languageManager.getMessage("general.spyMessage")), - ), - ), - ) - } + SpyPermissionManager.notifySpies( + noticeText = languageManager.getMessage("general.spyMessage"), + exclude = { it.uniqueId == playerId || it.uniqueId in memberIds }, + ) { formattedMessage } context.members.forEach { member -> Bukkit.getPlayer(member.playerId)?.let { memberPlayer -> if (memberPlayer.isOnline) { diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt index c3fc923..ba0b5f8 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt @@ -12,7 +12,6 @@ import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager import dev.m1sk9.lunaticChat.paper.velocity.RemotePlayerRegistry import net.kyori.adventure.text.Component import net.kyori.adventure.text.event.ClickEvent -import net.kyori.adventure.text.event.HoverEvent import org.bukkit.Bukkit import org.bukkit.entity.Player import java.util.UUID @@ -213,20 +212,12 @@ class DirectMessageHandler( recipientName: String, rawMessage: String, ) { - val spyMessage = formatMessage(format, senderName, recipientName, rawMessage, replyTo = senderName) - SpyPermissionManager - .getDirectMessageSpyPlayers() - .values - .filter { it.isOnline && it.name !in setOf(senderName, recipientName) } - .forEach { - it.sendMessage( - spyMessage.hoverEvent( - HoverEvent.showText( - Component.text(languageManager.getMessage("general.spyMessage")), - ), - ), - ) - } + SpyPermissionManager.notifySpies( + noticeText = languageManager.getMessage("general.spyMessage"), + exclude = { it.name == senderName || it.name == recipientName }, + ) { + formatMessage(format, senderName, recipientName, rawMessage, replyTo = senderName) + } } private fun formatMessage( diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt index ad0a9e0..fb96ba1 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt @@ -1,10 +1,10 @@ package dev.m1sk9.lunaticChat.paper.converter import dev.m1sk9.lunaticChat.engine.converter.CacheData -import dev.m1sk9.lunaticChat.paper.DebouncedSaver import kotlinx.serialization.json.Json import java.nio.file.Path 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.exists @@ -13,10 +13,10 @@ import kotlin.io.path.writeText class ConversionCache( private val cacheFile: Path, private val maxEntries: Int = 500, - private val saver: DebouncedSaver, private val logger: Logger, ) { private val conversionMemoryCache = ConcurrentHashMap<String, String>() + private val dirty = AtomicBoolean(false) companion object { private const val CACHE_VERSION = "1" @@ -81,16 +81,18 @@ class ConversionCache( } conversionMemoryCache[key] = value - saver.request(::saveToDisk) + dirty.set(true) } /** - * Saves the conversion cache from memory to disk. - * This operation is performed asynchronously. + * Writes the cache to disk if anything changed since the last write. * - * @throws Exception if an error occurs during the save operation. + * Called from the periodic task and at shutdown. Skipping a clean cache matters because the + * task fires on a fixed interval whether or not anyone chatted. */ fun saveToDisk() { + if (!dirty.getAndSet(false)) return + try { val data = CacheData( @@ -101,6 +103,7 @@ class ConversionCache( cacheFile.writeText(jsonBuffer) logger.info("Saved ${conversionMemoryCache.size} cache entries to disk.") } catch (e: Exception) { + dirty.set(true) logger.severe("Failed to save conversion cache to disk: ${e.message}") } } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/SpyPermissionManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/SpyPermissionManager.kt index 2c56219..2e50081 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/SpyPermissionManager.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/SpyPermissionManager.kt @@ -1,6 +1,8 @@ package dev.m1sk9.lunaticChat.paper.common import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.event.HoverEvent import org.bukkit.Bukkit import org.bukkit.entity.Player import org.bukkit.event.EventHandler @@ -23,6 +25,29 @@ object SpyPermissionManager : Listener { fun getDirectMessageSpyPlayers(): Map<UUID, Player> = directMessageSpyPlayers.toMap() /** + * Sends a copy of a message to every online spy that [exclude] does not reject. + * + * [message] is only invoked when someone will actually read the result, and the "you are + * seeing this because you have spy permission" hover is built once for the whole audience + * rather than per recipient. Spies are rare, so both matter on the message path. + * + * @param noticeText Text shown on hover, explaining why the reader is seeing the message + * @param exclude Rejects players who are party to the message already + * @param message Builds the message body + */ + fun notifySpies( + noticeText: String, + exclude: (Player) -> Boolean, + message: () -> Component, + ) { + val spies = directMessageSpyPlayers.values.filter { it.isOnline && !exclude(it) } + if (spies.isEmpty()) return + + val withNotice = message().hoverEvent(HoverEvent.showText(Component.text(noticeText))) + spies.forEach { it.sendMessage(withNotice) } + } + + /** * Updates the cache of players with direct message spy permission. * Call this on player join/quit/permission change events. */ |
