diff --git a/gradle.properties b/gradle.properties index 41dbdef7..5a4d1c56 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ kotlin.code.style=official kotlin.stdlib.default.dependency=false org.gradle.parallel=true -version=2.4.1 +version=2.4.2 diff --git a/surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/SurfCoreApi.kt b/surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/SurfCoreApi.kt index 4dce3011..fb85c48f 100644 --- a/surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/SurfCoreApi.kt +++ b/surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/SurfCoreApi.kt @@ -1,6 +1,7 @@ package dev.slne.surf.core.api.common import dev.slne.surf.api.core.util.requiredService +import dev.slne.surf.core.api.common.cache.OfflinePlayerNameCache import dev.slne.surf.core.api.common.event.SurfEvent import dev.slne.surf.core.api.common.player.SurfPlayer import dev.slne.surf.core.api.common.server.CommonSurfServer @@ -46,6 +47,8 @@ interface SurfCoreApi { fun fireEvent(event: SurfEvent) fun subscribe(eventClass: KClass, handler: (SurfEvent) -> Unit) + suspend fun loadOfflinePlayerNameEntries(): List + /** * Sends a request to connect the specified player to the given server and awaits the result. * This method can only send a player to a backend server, not a proxy. diff --git a/surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/cache/OfflinePlayerNameCache.kt b/surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/cache/OfflinePlayerNameCache.kt new file mode 100644 index 00000000..048d2af1 --- /dev/null +++ b/surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/cache/OfflinePlayerNameCache.kt @@ -0,0 +1,58 @@ +package dev.slne.surf.core.api.common.cache + +import dev.slne.surf.api.core.serializer.java.uuid.SerializableUUID +import dev.slne.surf.api.core.util.logger +import dev.slne.surf.core.api.common.SurfCoreApi +import kotlinx.coroutines.* +import kotlinx.serialization.Serializable +import kotlin.time.Duration.Companion.minutes + +object OfflinePlayerNameCache { + private val logger = logger() + + @Volatile + private var sortedNames: List = emptyList() + private val CACHE_REFRESH_INTERVAL = 5.minutes + + suspend fun refresh() { + sortedNames = SurfCoreApi.loadOfflinePlayerNameEntries().map { it.name } + .sortedWith(String.CASE_INSENSITIVE_ORDER) + } + + fun findByPrefix(prefix: String): List { + val lower = prefix.lowercase() + val snapshot = sortedNames + + var lo = 0 + var hi = snapshot.size + while (lo < hi) { + val mid = (lo + hi) ushr 1 + if (snapshot[mid].lowercase() < lower) lo = mid + 1 else hi = mid + } + + return buildList { + var i = lo + while (i < snapshot.size && snapshot[i].lowercase().startsWith(lower)) { + add(snapshot[i]) + i++ + } + } + } + + fun startPulling(instanceScope: CoroutineScope) = instanceScope.launch(Dispatchers.IO) { + while (isActive) { + runCatching { + refresh() + }.onFailure { + logger.atFine().log("Failed to refresh offline player name cache: ${it.message}") + } + delay(CACHE_REFRESH_INTERVAL) + } + } + + @Serializable + data class Entry( + val uuid: SerializableUUID, + val name: String + ) +} \ No newline at end of file diff --git a/surf-core-api/surf-core-api-paper/src/main/kotlin/dev/slne/surf/core/api/paper/command/argument/SurfOfflinePlayerArgument.kt b/surf-core-api/surf-core-api-paper/src/main/kotlin/dev/slne/surf/core/api/paper/command/argument/SurfOfflinePlayerArgument.kt index 6b258bd7..37ab3111 100644 --- a/surf-core-api/surf-core-api-paper/src/main/kotlin/dev/slne/surf/core/api/paper/command/argument/SurfOfflinePlayerArgument.kt +++ b/surf-core-api/surf-core-api-paper/src/main/kotlin/dev/slne/surf/core/api/paper/command/argument/SurfOfflinePlayerArgument.kt @@ -8,12 +8,15 @@ import dev.jorel.commandapi.arguments.CustomArgument import dev.jorel.commandapi.arguments.StringArgument import dev.slne.surf.api.core.util.logger import dev.slne.surf.core.api.common.SurfCoreApi +import dev.slne.surf.core.api.common.cache.OfflinePlayerNameCache import dev.slne.surf.core.api.common.player.SurfPlayer import dev.slne.surf.core.api.paper.CorePlayerStatusAccess import kotlinx.coroutines.* import kotlinx.coroutines.future.asDeferred import kotlinx.coroutines.future.future +private const val SUGGESTION_LIMIT = 500 + class SurfOfflinePlayerArgument(nodeName: String) : CustomArgument, String>(StringArgument(nodeName), { info -> scope.future { @@ -21,11 +24,22 @@ class SurfOfflinePlayerArgument(nodeName: String) : }.asDeferred() }) { init { - this.replaceSuggestions( - ArgumentSuggestions.stringCollection { viewerInfo -> - SurfCoreApi.getOnlinePlayers() - .filter { CorePlayerStatusAccess.hasAccess(viewerInfo.sender, it) } - .mapNotNull { it.lastKnownName } + replaceSuggestions( + ArgumentSuggestions.stringCollectionAsync { viewerInfo -> + scope.future { + val input = viewerInfo.currentArg ?: "" + + val onlinePlayerNames = SurfCoreApi.getOnlinePlayers() + .filter { CorePlayerStatusAccess.hasAccess(viewerInfo.sender, it) } + .mapNotNull { it.lastKnownName } + .filter { input.isEmpty() || it.startsWith(input, ignoreCase = true) } + + val onlineSet = onlinePlayerNames.toHashSet() + val offlinePlayerNames = OfflinePlayerNameCache.findByPrefix(input) + .filter { it !in onlineSet } + + (onlinePlayerNames + offlinePlayerNames).take(SUGGESTION_LIMIT) + } } ) } diff --git a/surf-core-api/surf-core-api-velocity/src/main/kotlin/dev/slne/surf/core/api/velocity/command/argument/SurfOfflinePlayerArgument.kt b/surf-core-api/surf-core-api-velocity/src/main/kotlin/dev/slne/surf/core/api/velocity/command/argument/SurfOfflinePlayerArgument.kt index d66684b1..c6d5e64b 100644 --- a/surf-core-api/surf-core-api-velocity/src/main/kotlin/dev/slne/surf/core/api/velocity/command/argument/SurfOfflinePlayerArgument.kt +++ b/surf-core-api/surf-core-api-velocity/src/main/kotlin/dev/slne/surf/core/api/velocity/command/argument/SurfOfflinePlayerArgument.kt @@ -10,11 +10,14 @@ import dev.jorel.commandapi.arguments.CommandAPIArgumentType import dev.jorel.commandapi.executors.CommandArguments import dev.slne.surf.api.core.util.logger import dev.slne.surf.core.api.common.SurfCoreApi +import dev.slne.surf.core.api.common.cache.OfflinePlayerNameCache import dev.slne.surf.core.api.common.player.SurfPlayer import kotlinx.coroutines.* import kotlinx.coroutines.future.asDeferred import kotlinx.coroutines.future.future +private const val SUGGESTION_LIMIT = 500 + @Suppress("UNCHECKED_CAST") open class SurfOfflinePlayerArgument(nodeName: String) : Argument>(nodeName, StringArgumentType::string) { @@ -36,9 +39,20 @@ open class SurfOfflinePlayerArgument(nodeName: String) : init { replaceSuggestions( - ArgumentSuggestions.stringCollection { _ -> - SurfCoreApi.getOnlinePlayers() - .mapNotNull { it.lastKnownName } + ArgumentSuggestions.stringCollectionAsync { viewerInfo -> + scope.future { + val input = viewerInfo.currentArg ?: "" + + val onlineNames = SurfCoreApi.getOnlinePlayers() + .mapNotNull { it.lastKnownName } + .filter { input.isEmpty() || it.startsWith(input, ignoreCase = true) } + + val onlinePlayerNames = onlineNames.toHashSet() + val offlinePlayerNames = OfflinePlayerNameCache.findByPrefix(input) + .filter { it !in onlinePlayerNames } + + (onlineNames + offlinePlayerNames).take(SUGGESTION_LIMIT) + } } ) } diff --git a/surf-core-core/surf-core-core-client/src/main/kotlin/dev/slne/surf/core/client/SurfCoreApiClientImpl.kt b/surf-core-core/surf-core-core-client/src/main/kotlin/dev/slne/surf/core/client/SurfCoreApiClientImpl.kt new file mode 100644 index 00000000..456c9cf4 --- /dev/null +++ b/surf-core-core/surf-core-core-client/src/main/kotlin/dev/slne/surf/core/client/SurfCoreApiClientImpl.kt @@ -0,0 +1,9 @@ +package dev.slne.surf.core.client + +import dev.slne.surf.core.core.common.SurfCoreApiImpl +import dev.slne.surf.core.core.common.rabbit.packet.player.load.LoadOfflinePlayerNameEntriesRequestPacket + +abstract class SurfCoreApiClientImpl : SurfCoreApiImpl() { + override suspend fun loadOfflinePlayerNameEntries() = + ClientCoreInstance.rabbitApi.sendRequest(LoadOfflinePlayerNameEntriesRequestPacket()).entries +} \ No newline at end of file diff --git a/surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/rabbit/packet/player/ManyOfflinePlayerNamesResponsePacket.kt b/surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/rabbit/packet/player/ManyOfflinePlayerNamesResponsePacket.kt new file mode 100644 index 00000000..4e1796f5 --- /dev/null +++ b/surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/rabbit/packet/player/ManyOfflinePlayerNamesResponsePacket.kt @@ -0,0 +1,10 @@ +package dev.slne.surf.core.core.common.rabbit.packet.player + +import dev.slne.surf.core.api.common.cache.OfflinePlayerNameCache +import dev.slne.surf.rabbitmq.api.packet.RabbitResponsePacket +import kotlinx.serialization.Serializable + +@Serializable +data class ManyOfflinePlayerNamesResponsePacket( + val entries: List +) : RabbitResponsePacket() diff --git a/surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/rabbit/packet/player/load/LoadOfflinePlayerNameEntriesRequestPacket.kt b/surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/rabbit/packet/player/load/LoadOfflinePlayerNameEntriesRequestPacket.kt new file mode 100644 index 00000000..a4af7876 --- /dev/null +++ b/surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/rabbit/packet/player/load/LoadOfflinePlayerNameEntriesRequestPacket.kt @@ -0,0 +1,9 @@ +package dev.slne.surf.core.core.common.rabbit.packet.player.load + +import dev.slne.surf.core.core.common.rabbit.packet.player.ManyOfflinePlayerNamesResponsePacket +import dev.slne.surf.rabbitmq.api.packet.RabbitRequestPacket +import kotlinx.serialization.Serializable + +@Serializable +class LoadOfflinePlayerNameEntriesRequestPacket : + RabbitRequestPacket() diff --git a/surf-core-microservice/src/main/kotlin/dev/slne/surf/core/microservice/SurfCoreApiMicroserviceImpl.kt b/surf-core-microservice/src/main/kotlin/dev/slne/surf/core/microservice/SurfCoreApiMicroserviceImpl.kt index 7affcd7a..936b7ed9 100644 --- a/surf-core-microservice/src/main/kotlin/dev/slne/surf/core/microservice/SurfCoreApiMicroserviceImpl.kt +++ b/surf-core-microservice/src/main/kotlin/dev/slne/surf/core/microservice/SurfCoreApiMicroserviceImpl.kt @@ -9,4 +9,6 @@ class SurfCoreApiMicroserviceImpl : SurfCoreApiImpl() { override fun getCurrentServerName() = "surf-core" override fun getCurrentServerDisplayName() = "surf-core" override fun getCurrentServerCategory() = "microservice" + + override suspend fun loadOfflinePlayerNameEntries() = error("Not available on microservice") } \ No newline at end of file diff --git a/surf-core-microservice/src/main/kotlin/dev/slne/surf/core/microservice/database/repository/SurfPlayerRepository.kt b/surf-core-microservice/src/main/kotlin/dev/slne/surf/core/microservice/database/repository/SurfPlayerRepository.kt index 1f01d841..c8745d9e 100644 --- a/surf-core-microservice/src/main/kotlin/dev/slne/surf/core/microservice/database/repository/SurfPlayerRepository.kt +++ b/surf-core-microservice/src/main/kotlin/dev/slne/surf/core/microservice/database/repository/SurfPlayerRepository.kt @@ -1,13 +1,17 @@ package dev.slne.surf.core.microservice.database.repository +import dev.slne.surf.core.api.common.cache.OfflinePlayerNameCache import dev.slne.surf.core.api.common.player.SurfPlayer import dev.slne.surf.core.microservice.database.tables.SurfPlayersTable import dev.slne.surf.database.libs.org.jetbrains.exposed.v1.core.ResultRow import dev.slne.surf.database.libs.org.jetbrains.exposed.v1.core.eq +import dev.slne.surf.database.libs.org.jetbrains.exposed.v1.r2dbc.select import dev.slne.surf.database.libs.org.jetbrains.exposed.v1.r2dbc.selectAll import dev.slne.surf.database.libs.org.jetbrains.exposed.v1.r2dbc.transactions.suspendTransaction import dev.slne.surf.database.libs.org.jetbrains.exposed.v1.r2dbc.upsert import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.toList import java.time.OffsetDateTime import java.util.* @@ -47,6 +51,22 @@ object SurfPlayerRepository { Unit } + suspend fun loadOfflinePlayerNameEntries() = suspendTransaction { + SurfPlayersTable.select(SurfPlayersTable.name, SurfPlayersTable.uuid).mapNotNull { + val name = it[SurfPlayersTable.name] + val uuid = it[SurfPlayersTable.uuid] + + if (name == null) { + return@mapNotNull null + } + + OfflinePlayerNameCache.Entry( + uuid, + name + ) + }.toList() + } + private fun createPlayerByRow(row: ResultRow) = SurfPlayer( uuid = row[SurfPlayersTable.uuid], lastKnownName = row[SurfPlayersTable.name], diff --git a/surf-core-microservice/src/main/kotlin/dev/slne/surf/core/microservice/rabbit/SurfPlayerHandler.kt b/surf-core-microservice/src/main/kotlin/dev/slne/surf/core/microservice/rabbit/SurfPlayerHandler.kt index d888a16e..aabce2f8 100644 --- a/surf-core-microservice/src/main/kotlin/dev/slne/surf/core/microservice/rabbit/SurfPlayerHandler.kt +++ b/surf-core-microservice/src/main/kotlin/dev/slne/surf/core/microservice/rabbit/SurfPlayerHandler.kt @@ -1,6 +1,8 @@ package dev.slne.surf.core.microservice.rabbit +import dev.slne.surf.core.core.common.rabbit.packet.player.ManyOfflinePlayerNamesResponsePacket import dev.slne.surf.core.core.common.rabbit.packet.player.OptionalSurfPlayerResponsePacket +import dev.slne.surf.core.core.common.rabbit.packet.player.load.LoadOfflinePlayerNameEntriesRequestPacket import dev.slne.surf.core.core.common.rabbit.packet.player.load.LoadPlayerByNameRequestPacket import dev.slne.surf.core.core.common.rabbit.packet.player.load.LoadPlayerByUuidRequestPacket import dev.slne.surf.core.core.common.rabbit.packet.player.save.SaveSurfPlayerRequestPacket @@ -51,4 +53,14 @@ object SurfPlayerHandler { ) } } + + @RabbitHandler + fun handleLoadOfflinePlayerNameEntriesRequest(request: LoadOfflinePlayerNameEntriesRequestPacket) = + request.launch { + request.respond( + ManyOfflinePlayerNamesResponsePacket( + SurfPlayerRepository.loadOfflinePlayerNameEntries() + ) + ) + } } \ No newline at end of file diff --git a/surf-core-paper/src/main/kotlin/dev/slne/surf/core/paper/PaperMain.kt b/surf-core-paper/src/main/kotlin/dev/slne/surf/core/paper/PaperMain.kt index bf56c916..64e232b2 100644 --- a/surf-core-paper/src/main/kotlin/dev/slne/surf/core/paper/PaperMain.kt +++ b/surf-core-paper/src/main/kotlin/dev/slne/surf/core/paper/PaperMain.kt @@ -1,9 +1,11 @@ package dev.slne.surf.core.paper import com.github.shynixn.mccoroutine.folia.SuspendingJavaPlugin +import com.github.shynixn.mccoroutine.folia.scope import dev.slne.surf.api.core.luckperms.LuckPermsAccess import dev.slne.surf.api.paper.event.register import dev.slne.surf.api.paper.util.getPrefixedName +import dev.slne.surf.core.api.common.cache.OfflinePlayerNameCache import dev.slne.surf.core.api.common.event.SurfServerOnlineEvent import dev.slne.surf.core.api.common.event.SurfServerStoppingEvent import dev.slne.surf.core.api.common.server.SurfServer @@ -54,6 +56,7 @@ class PaperMain : SuspendingJavaPlugin() { luckPerms() surfServerInformationSyncTask.start() + OfflinePlayerNameCache.startPulling(plugin.scope) } override suspend fun onDisableAsync() { diff --git a/surf-core-paper/src/main/kotlin/dev/slne/surf/core/paper/api/SurfCoreApiPaperImpl.kt b/surf-core-paper/src/main/kotlin/dev/slne/surf/core/paper/api/SurfCoreApiPaperImpl.kt index dad70993..0b8bf1f4 100644 --- a/surf-core-paper/src/main/kotlin/dev/slne/surf/core/paper/api/SurfCoreApiPaperImpl.kt +++ b/surf-core-paper/src/main/kotlin/dev/slne/surf/core/paper/api/SurfCoreApiPaperImpl.kt @@ -2,12 +2,12 @@ package dev.slne.surf.core.paper.api import com.google.auto.service.AutoService import dev.slne.surf.core.api.common.SurfCoreApi -import dev.slne.surf.core.core.common.SurfCoreApiImpl +import dev.slne.surf.core.client.SurfCoreApiClientImpl import dev.slne.surf.core.paper.surfServerConfig import net.kyori.adventure.util.Services @AutoService(SurfCoreApi::class) -class SurfCoreApiPaperImpl : SurfCoreApiImpl(), Services.Fallback { +class SurfCoreApiPaperImpl : SurfCoreApiClientImpl(), Services.Fallback { override fun getCurrentServerName() = surfServerConfig.serverName override fun getCurrentServerCategory() = surfServerConfig.serverCategory override fun getCurrentServerDisplayName() = surfServerConfig.serverDisplayName diff --git a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/VelocityMain.kt b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/VelocityMain.kt index db21f718..a7de4d15 100644 --- a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/VelocityMain.kt +++ b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/VelocityMain.kt @@ -2,6 +2,7 @@ package dev.slne.surf.core.velocity import com.github.shynixn.mccoroutine.velocity.SuspendingPluginContainer import com.github.shynixn.mccoroutine.velocity.registerSuspend +import com.github.shynixn.mccoroutine.velocity.scope import com.google.inject.Inject import com.velocitypowered.api.event.EventManager import com.velocitypowered.api.event.Subscribe @@ -13,6 +14,7 @@ import com.velocitypowered.api.plugin.annotation.DataDirectory import com.velocitypowered.api.proxy.ProxyServer import dev.slne.surf.api.core.messages.adventure.buildText import dev.slne.surf.core.api.common.SurfCoreApi +import dev.slne.surf.core.api.common.cache.OfflinePlayerNameCache import dev.slne.surf.core.api.common.event.SurfServerOnlineEvent import dev.slne.surf.core.api.common.event.SurfServerStartEvent import dev.slne.surf.core.api.common.event.SurfServerStoppingEvent @@ -103,6 +105,7 @@ class VelocityMain @Inject constructor( coreCommand() surfPlayerSyncTask.start() + OfflinePlayerNameCache.startPulling(pluginContainer.scope) } @Subscribe diff --git a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/api/SurfCoreApiVelocityImpl.kt b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/api/SurfCoreApiVelocityImpl.kt index 8804f767..0c6f09cf 100644 --- a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/api/SurfCoreApiVelocityImpl.kt +++ b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/api/SurfCoreApiVelocityImpl.kt @@ -5,7 +5,7 @@ import dev.slne.surf.core.api.common.SurfCoreApi import dev.slne.surf.core.api.common.player.SurfPlayer import dev.slne.surf.core.api.common.server.SurfServer import dev.slne.surf.core.api.common.server.connection.SurfServerConnectResult -import dev.slne.surf.core.core.common.SurfCoreApiImpl +import dev.slne.surf.core.client.SurfCoreApiClientImpl import dev.slne.surf.core.velocity.plugin import dev.slne.surf.core.velocity.redis.handler.convertResult import dev.slne.surf.core.velocity.surfServerConfig @@ -14,7 +14,7 @@ import net.kyori.adventure.util.Services import kotlin.jvm.optionals.getOrNull @AutoService(SurfCoreApi::class) -class SurfCoreApiVelocityImpl : SurfCoreApiImpl(), Services.Fallback { +class SurfCoreApiVelocityImpl : SurfCoreApiClientImpl(), Services.Fallback { override fun getCurrentServerName() = surfServerConfig.serverName override fun getCurrentServerCategory() = surfServerConfig.serverCategory override fun getCurrentServerDisplayName() = surfServerConfig.serverDisplayName @@ -32,7 +32,7 @@ class SurfCoreApiVelocityImpl : SurfCoreApiImpl(), Services.Fallback { SurfServerConnectResult.Status.SERVER_NOT_FOUND, null ) - + return player.createConnectionRequest(velocityServer) .connect() .await()