summaryrefslogtreecommitdiff
path: root/platform-paper
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-08-03 00:45:44 +0900
committerSho Sakuma <me@m1sk9.dev>2026-08-05 00:42:23 +0900
commit790dcf942a3acab1268d46e3fc22be25a4ee02d5 (patch)
tree386234a8c0fbbc37a4d606f8b6db165ab95f0444 /platform-paper
parent1ee7a7fb0b24a99daace60016468774a5bd6fbf4 (diff)
downloadLunaticChat-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')
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt1
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt18
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/DirectMessageHandler.kt21
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/ConversionCache.kt15
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/SpyPermissionManager.kt25
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.
*/