diff --git a/gradle.properties b/gradle.properties index 35e92acc..7ba7bd00 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.3.2 +version=2.4.0 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 8bdaf60c..b1b8ef56 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ec2e3c24..b52fb7e7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,9 @@ -#Sun Apr 05 17:57:20 CEST 2026 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index adff685a..b9bb139f 100755 --- a/gradlew +++ b/gradlew @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. diff --git a/gradlew.bat b/gradlew.bat index c4bdd3ab..24c62d56 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -23,8 +23,8 @@ @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -51,7 +51,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,7 +65,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line @@ -73,21 +73,10 @@ goto fail @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/settings.gradle.kts b/settings.gradle.kts index c6904527..3f56fae5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,3 +29,7 @@ include("surf-core-velocity") // Microservice include("surf-core-microservice") + +include("surf-core-launcher") +include("surf-core-launcher:surf-core-launcher-api") +include("surf-core-launcher:surf-core-launcher-server") \ No newline at end of file diff --git a/surf-core-api/surf-core-api-common/build.gradle.kts b/surf-core-api/surf-core-api-common/build.gradle.kts index 6c26e093..9a245056 100644 --- a/surf-core-api/surf-core-api-common/build.gradle.kts +++ b/surf-core-api/surf-core-api-common/build.gradle.kts @@ -4,6 +4,10 @@ plugins { id("dev.slne.surf.api.gradle.core") } +surfCoreApi { + withSurfRedis() +} + publishing { repositories { slneReleases() diff --git a/surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/redis/event/SurfEventFireRedisEvent.kt b/surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/event/redis/SurfEventFireRedisEvent.kt similarity index 81% rename from surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/redis/event/SurfEventFireRedisEvent.kt rename to surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/event/redis/SurfEventFireRedisEvent.kt index 7edc03f0..8d203bd4 100644 --- a/surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/redis/event/SurfEventFireRedisEvent.kt +++ b/surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/event/redis/SurfEventFireRedisEvent.kt @@ -1,4 +1,4 @@ -package dev.slne.surf.core.core.common.redis.event +package dev.slne.surf.core.api.common.event.redis import dev.slne.surf.core.api.common.event.SurfEvent import dev.slne.surf.redis.event.RedisEvent diff --git a/surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/server/state/SurfServiceStatus.kt b/surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/server/state/SurfServiceStatus.kt new file mode 100644 index 00000000..711284a0 --- /dev/null +++ b/surf-core-api/surf-core-api-common/src/main/kotlin/dev/slne/surf/core/api/common/server/state/SurfServiceStatus.kt @@ -0,0 +1,6 @@ +package dev.slne.surf.core.api.common.server.state + +enum class SurfServiceStatus { + UNREACHABLE, + CRASHED +} \ 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/event/LocalSurfEventBusListener.kt b/surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/event/LocalSurfEventBusListener.kt index 6e78bb17..e59e3966 100644 --- a/surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/event/LocalSurfEventBusListener.kt +++ b/surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/event/LocalSurfEventBusListener.kt @@ -1,6 +1,6 @@ package dev.slne.surf.core.core.common.event -import dev.slne.surf.core.core.common.redis.event.SurfEventFireRedisEvent +import dev.slne.surf.core.api.common.event.redis.SurfEventFireRedisEvent import dev.slne.surf.redis.event.OnRedisEvent object LocalSurfEventBusListener { diff --git a/surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/event/SurfEventBus.kt b/surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/event/SurfEventBus.kt index 82bcd76d..2248af73 100644 --- a/surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/event/SurfEventBus.kt +++ b/surf-core-core/surf-core-core-common/src/main/kotlin/dev/slne/surf/core/core/common/event/SurfEventBus.kt @@ -2,8 +2,8 @@ package dev.slne.surf.core.core.common.event import dev.slne.surf.core.api.common.event.SurfEvent import dev.slne.surf.core.api.common.event.SurfEventHandler +import dev.slne.surf.core.api.common.event.redis.SurfEventFireRedisEvent import dev.slne.surf.core.core.CoreInstance -import dev.slne.surf.core.core.common.redis.event.SurfEventFireRedisEvent import kotlin.reflect.KClass import kotlin.reflect.full.declaredFunctions import kotlin.reflect.full.findAnnotation diff --git a/surf-core-launcher/surf-core-launcher-api/build.gradle.kts b/surf-core-launcher/surf-core-launcher-api/build.gradle.kts new file mode 100644 index 00000000..2654a79c --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-api/build.gradle.kts @@ -0,0 +1,19 @@ +import dev.slne.surf.api.gradle.util.slneReleases + +plugins { + id("dev.slne.surf.api.gradle.core") +} + +surfCoreApi { + withSurfRedis() +} + +dependencies { + api(projects.surfCoreApi.surfCoreApiCommon) +} + +publishing { + repositories { + slneReleases() + } +} \ No newline at end of file diff --git a/surf-core-launcher/surf-core-launcher-api/src/main/kotlin/dev/slne/surf/core/launcher/api/LauncherConstants.kt b/surf-core-launcher/surf-core-launcher-api/src/main/kotlin/dev/slne/surf/core/launcher/api/LauncherConstants.kt new file mode 100644 index 00000000..09982dda --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-api/src/main/kotlin/dev/slne/surf/core/launcher/api/LauncherConstants.kt @@ -0,0 +1,5 @@ +package dev.slne.surf.core.launcher.api + +object LauncherConstants { + const val PROPERTY_LAUNCHED_BY_CORE = "LAUNCHED_BY_CORE" +} \ No newline at end of file diff --git a/surf-core-launcher/surf-core-launcher-api/src/main/kotlin/dev/slne/surf/core/launcher/api/redis/ServiceStatusRedisEvent.kt b/surf-core-launcher/surf-core-launcher-api/src/main/kotlin/dev/slne/surf/core/launcher/api/redis/ServiceStatusRedisEvent.kt new file mode 100644 index 00000000..4ea3e20d --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-api/src/main/kotlin/dev/slne/surf/core/launcher/api/redis/ServiceStatusRedisEvent.kt @@ -0,0 +1,11 @@ +package dev.slne.surf.core.launcher.api.redis + +import dev.slne.surf.core.api.common.server.state.SurfServiceStatus +import dev.slne.surf.redis.event.RedisEvent +import kotlinx.serialization.Serializable + +@Serializable +data class ServiceStatusRedisEvent( + val serviceName: String, + val status: SurfServiceStatus +) : RedisEvent() diff --git a/surf-core-launcher/surf-core-launcher-server/build.gradle.kts b/surf-core-launcher/surf-core-launcher-server/build.gradle.kts new file mode 100644 index 00000000..5e8c3527 --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-server/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + id("dev.slne.surf.api.gradle.standalone") +} + +dependencies { + api(projects.surfCoreLauncher.surfCoreLauncherApi) + implementation(projects.surfCoreApi.surfCoreApiCommon) + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.2") + implementation("dev.slne.surf.redis:surf-redis-standalone:1.6.1") +} + +tasks.jar { + manifest { + attributes["Main-Class"] = "dev.slne.surf.core.launcher.server.CoreLauncherKt" + } +} + +tasks.shadowJar { + exclude("okio/**") + exclude("io/netty/**") +} \ No newline at end of file diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt new file mode 100644 index 00000000..862bb96a --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncher.kt @@ -0,0 +1,186 @@ +package dev.slne.surf.core.launcher.server + +import dev.slne.surf.api.standalone.SurfApiStandaloneBootstrap +import dev.slne.surf.core.api.common.event.SurfServerStartEvent +import dev.slne.surf.core.api.common.event.redis.SurfEventFireRedisEvent +import dev.slne.surf.core.launcher.api.LauncherConstants +import dev.slne.surf.core.launcher.server.config.CoreLauncherConfig +import dev.slne.surf.core.launcher.server.ping.MinecraftServerPinger +import dev.slne.surf.core.launcher.server.updater.process.PluginUpdater +import dev.slne.surf.redis.RedisApi +import dev.slne.surf.redis.StandaloneRedisInstance +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow +import java.nio.file.Path +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.io.path.Path +import kotlin.time.Duration.Companion.seconds + +private val secondDateTimeFormatter = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm:ss") + +val LOG_PREFIX + get() = + "\u001B[0;91m[${ + LocalDateTime.now().format(secondDateTimeFormatter) + } CoreLauncher]\u001B[0m" + +object CoreLauncher { + private val shuttingDown = AtomicBoolean(false) + lateinit var serverProcess: Process + + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private var monitorJob: Job? = null + private val redisInstance = StandaloneRedisInstance( + name = "surf-core-launcher", + configPath = findRedisPluginPath() + ?: error("Could not find Redis plugin configuration path, cannot start Redis instance") + ) + lateinit var redisApi: RedisApi + + val serverOnline = MutableStateFlow(false) + + val config by lazy { + CoreLauncherConfig.getConfig() + } + + suspend fun launch() = withContext(Dispatchers.IO) { + SurfApiStandaloneBootstrap.bootstrap() + SurfApiStandaloneBootstrap.enable() + + println("$LOG_PREFIX Initializing Redis instance...") + + redisInstance.create() + redisApi = RedisApi.create() + redisApi.freezeAndConnect() + + println("$LOG_PREFIX Redis instance initialized and connected") + + if (config.autoUpdateSurfPlugins) { + println("$LOG_PREFIX Searching plugin updates...") + + withTimeoutOrNull(20.seconds) { + if (config.personalAccessToken.isBlank()) { + println("$LOG_PREFIX No GitHub personal access token provided, skipping plugin update check") + return@withTimeoutOrNull + } + + PluginUpdater.start() + } + ?: println("$LOG_PREFIX Plugin update check timed out after 20 seconds, continuing with server startup") + } + + println("$LOG_PREFIX Starting Minecraft Server...") + + val command = buildStartupCommand() + + serverProcess = ProcessBuilder(command) + .redirectInput(ProcessBuilder.Redirect.INHERIT) + .redirectOutput(ProcessBuilder.Redirect.PIPE) + .redirectError(ProcessBuilder.Redirect.INHERIT) + .start() + + println("$LOG_PREFIX Server process started") + + redisApi.publishEvent( + SurfEventFireRedisEvent( + SurfServerStartEvent( + serverName = config.serverName + ) + ) + ) + + monitorJob = scope.launch { + launch { + serverProcess.inputStream.bufferedReader().forEachLine { line -> + println(line) + + if (line.contains( + config.startedMessage, + ignoreCase = true + ) + ) { + serverOnline.value = true + println("$LOG_PREFIX Server is now online.") + } + } + } + + MinecraftServerPinger.monitor(serverProcess) + } + } + + suspend fun shutdown() { + if (!shuttingDown.compareAndSet(false, true)) { + return + } + + serverOnline.value = true + + println("$LOG_PREFIX Shutting down launcher/server...") + println("$LOG_PREFIX Disconnecting Redis instance...") + + redisApi.disconnect() + redisInstance.shutdown() + println("$LOG_PREFIX Redis instance disconnected and shutdown") + + monitorJob?.cancelAndJoin() + + if (::serverProcess.isInitialized && serverProcess.isAlive) { + serverProcess.destroy() + + if (!serverProcess.waitFor(45, TimeUnit.SECONDS)) { + println("$LOG_PREFIX Server did not stop within 45 seconds") + } + } + + scope.cancel() + } + + fun isShuttingDown(): Boolean = shuttingDown.get() + + private fun buildStartupCommand(): List { + val base = config.serverStartupCommand + + val parts = Regex("""[^\s"]+|"([^"]*)"""") + .findAll(base) + .map { it.value.replace("\"", "") } + .toMutableList() + + val flag = "-D${LauncherConstants.PROPERTY_LAUNCHED_BY_CORE}" + + if (parts.none { it == flag }) { + parts.add(1, flag) + } + + return parts + } + + private fun findRedisPluginPath(): Path? { + val possiblePaths = listOf( + Path("plugins", "surf-redis-paper"), + Path("plugins", "surf-redis-velocity") + ) + + return possiblePaths.firstOrNull { path -> + path.toFile().exists() + } + } +} + +suspend fun main(args: Array) { + Runtime.getRuntime().addShutdownHook(Thread { + runBlocking { + CoreLauncher.shutdown() + SurfApiStandaloneBootstrap.shutdown() + } + }) + + CoreLauncher.launch() + CoreLauncher.serverProcess.waitFor() + + CoreLauncher.shutdown() + SurfApiStandaloneBootstrap.shutdown() +} \ No newline at end of file diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncherEnvironment.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncherEnvironment.kt new file mode 100644 index 00000000..4bbea402 --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/CoreLauncherEnvironment.kt @@ -0,0 +1,8 @@ +package dev.slne.surf.core.launcher.server + +object CoreLauncherEnvironment { + val MC_MEMORY_MAX = + System.getenv("SERVER_MEMORY") ?: error("SERVER_MEMORY environment variable is not set") + val SERVER_PORT = System.getenv("SERVER_PORT")?.toInt() + ?: error("SERVER_PORT environment variable is not set or is not a valid integer") +} \ No newline at end of file diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/config/CoreLauncherConfig.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/config/CoreLauncherConfig.kt new file mode 100644 index 00000000..1b8f885b --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/config/CoreLauncherConfig.kt @@ -0,0 +1,26 @@ +package dev.slne.surf.core.launcher.server.config + +import dev.slne.surf.api.core.config.SpongeYmlConfigClass +import dev.slne.surf.api.core.serializer.java.uuid.SerializableUUID +import org.spongepowered.configurate.objectmapping.ConfigSerializable +import java.util.* +import kotlin.io.path.Path + +@ConfigSerializable +data class CoreLauncherConfig( + val serverStartupCommand: String = "java -jar server.jar --nogui", + val startedMessage: String = "For help, type \"help\"", + val serverName: String = "unknown", + val serverDisplayName: String = "unknown", + val serverCategory: String = "unknown", + val serverUuid: SerializableUUID = UUID.randomUUID(), + val autoUpdateSurfPlugins: Boolean = true, + val personalAccessToken: String = "", + val autoUpdateIgnoredPlugins: List = listOf("surf-example-paper") +) { + companion object : SpongeYmlConfigClass( + CoreLauncherConfig::class.java, + Path("."), + "core-launcher-config.yml" + ) +} diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/MinecraftServerPinger.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/MinecraftServerPinger.kt new file mode 100644 index 00000000..9f84bf8f --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/MinecraftServerPinger.kt @@ -0,0 +1,105 @@ +package dev.slne.surf.core.launcher.server.ping + +import dev.slne.surf.core.api.common.server.state.SurfServiceStatus +import dev.slne.surf.core.launcher.api.redis.ServiceStatusRedisEvent +import dev.slne.surf.core.launcher.server.CoreLauncher +import dev.slne.surf.core.launcher.server.CoreLauncherEnvironment +import dev.slne.surf.core.launcher.server.LOG_PREFIX +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.first +import java.io.DataInputStream +import java.io.DataOutputStream +import java.net.InetSocketAddress +import java.net.Socket +import kotlin.time.Duration.Companion.seconds + +object MinecraftServerPinger { + suspend fun monitor(process: Process): Unit = coroutineScope { + launch { + val exitCode = process.waitFor() + if (CoreLauncher.isShuttingDown()) { + return@launch + } + + if (exitCode == 0) { + println("$LOG_PREFIX Minecraft server stopped") + } else { + println("$LOG_PREFIX Minecraft server crashed!") + CoreLauncher.redisApi.publishEvent( + ServiceStatusRedisEvent( + serviceName = CoreLauncher.config.serverName, + status = SurfServiceStatus.CRASHED + ) + ) + } + } + + launch { + CoreLauncher.serverOnline.first { it } + + if (!process.isAlive) { + return@launch + } + + while (process.isAlive && !CoreLauncher.isShuttingDown()) { + delay(3.seconds) + + if (!CoreLauncher.serverOnline.value) { + continue + } + + val reachable = withContext(Dispatchers.IO) { + isMinecraftReady(CoreLauncherEnvironment.SERVER_PORT) + } + + if (!reachable) { + println("$LOG_PREFIX Minecraft server is unreachable") + CoreLauncher.redisApi.publishEvent( + ServiceStatusRedisEvent( + serviceName = CoreLauncher.config.serverName, + status = SurfServiceStatus.UNREACHABLE + ) + ) + } + } + } + + awaitCancellation() + } + + private fun isMinecraftReady(port: Int): Boolean = + runCatching { + Socket().use { socket -> + val host = "127.0.0.1" + socket.connect(InetSocketAddress(host, port), 1500) + socket.soTimeout = 1500 + + DataOutputStream(socket.getOutputStream()).use { output -> + DataInputStream(socket.getInputStream()).use { input -> + output.write(buildHandshakePacket(host, port)) + output.write(byteArrayOf(0x01, 0x00)) // Status request + output.flush() + + readVarInt(input) // Packet length + val packetId = readVarInt(input) // Packet ID + + packetId == 0x00 // 0x00 = gültiger Status Response + } + } + } + }.getOrDefault(false) + + private fun readVarInt(input: DataInputStream): Int { + var value = 0 + var position = 0 + while (true) { + val byte = input.readByte().toInt() + value = value or ((byte and 0x7F) shl position) + if (byte and 0x80 == 0) break + position += 7 + if (position >= 35) error("VarInt too large") + } + return value + } + +} \ No newline at end of file diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/ping-util.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/ping-util.kt new file mode 100644 index 00000000..214e60b4 --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/ping/ping-util.kt @@ -0,0 +1,45 @@ +package dev.slne.surf.core.launcher.server.ping + +@Suppress("SameParameterValue") +fun buildHandshakePacket( + host: String, + port: Int +): ByteArray { + val hostBytes = host.toByteArray() + + val data = mutableListOf() + + data += 0x00 + data += writeVarInt(763).toList() + + data += writeVarInt(hostBytes.size).toList() + data += hostBytes.toList() + + data += ((port shr 8) and 0xFF).toByte() + data += (port and 0xFF).toByte() + + data += 0x01 + + val packetLength = writeVarInt(data.size) + + return packetLength + data.toByteArray() +} + +fun writeVarInt(value: Int): ByteArray { + var current = value + val output = mutableListOf() + + do { + var temp = (current and 0b01111111) + + current = current ushr 7 + + if (current != 0) { + temp = temp or 0b10000000 + } + + output += temp.toByte() + } while (current != 0) + + return output.toByteArray() +} \ No newline at end of file diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/UpdatablePlugin.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/UpdatablePlugin.kt new file mode 100644 index 00000000..ac53277f --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/UpdatablePlugin.kt @@ -0,0 +1,36 @@ +package dev.slne.surf.core.launcher.server.updater + +import java.nio.file.Path + + +/** + * Represents a plugin that can be updated. + * + * @property name The name of the plugin, e.g. surf-core-paper, surf-api-paper-server, surf-captcha or surf-core-velocity. + * @property currentVersion The current version of the plugin. + */ +data class UpdatablePlugin( + val name: String, + val currentVersion: String, + val jarPath: Path +) { + /** + * The Plugin name, e.g. core, captcha or api + */ + fun findPluginName(): String { + if (name == "surf-paper-api") { // Special case for the old API plugin name + return "api" + } + + return name.substringAfter("surf-").substringBefore("-paper").substringBefore("-velocity") + .replace("-server", "").replace("-api", "") + } + + fun findPluginType() = when { + name.contains("-paper") -> "paper" + name.contains("-velocity") -> "velocity" + else -> null + } + + val latestReleaseUrl get() = "https://api.github.com/repos/SLNE-DEVELOPMENT/surf-${findPluginName()}/releases/latest" +} diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/cooldown/UpdateCooldownTracker.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/cooldown/UpdateCooldownTracker.kt new file mode 100644 index 00000000..906d4897 --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/cooldown/UpdateCooldownTracker.kt @@ -0,0 +1,52 @@ +package dev.slne.surf.core.launcher.server.updater.cooldown + +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardOpenOption +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit + +class UpdateCooldownTracker(private val persistencePath: Path) { + private val lastUpdated = ConcurrentHashMap() + private val cooldownMs = TimeUnit.MINUTES.toMillis(10) + + init { + load() + } + + fun isOnCooldown(pluginName: String): Boolean { + val lastUpdate = lastUpdated[pluginName] ?: return false + return System.currentTimeMillis() - lastUpdate < cooldownMs + } + + fun remainingSeconds(pluginName: String): Long { + val lastUpdate = lastUpdated[pluginName] ?: return 0 + return (cooldownMs - (System.currentTimeMillis() - lastUpdate)) / 1000 + } + + fun markUpdated(pluginName: String) { + lastUpdated[pluginName] = System.currentTimeMillis() + save() + } + + private fun load() { + if (!Files.exists(persistencePath)) { + return + } + + Files.readAllLines(persistencePath).forEach { line -> + val parts = line.split("=", limit = 2) + if (parts.size != 2) return@forEach + lastUpdated[parts[0].trim()] = parts[1].trim().toLongOrNull() ?: return@forEach + } + } + + private fun save() { + Files.writeString( + persistencePath, + lastUpdated.entries.joinToString("\n") { (name, ts) -> "$name=$ts" }, + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING + ) + } +} \ No newline at end of file diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/github/GitHubClient.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/github/GitHubClient.kt new file mode 100644 index 00000000..44f9ef4d --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/github/GitHubClient.kt @@ -0,0 +1,47 @@ +package dev.slne.surf.core.launcher.server.updater.github + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import dev.slne.surf.core.launcher.server.updater.UpdatablePlugin +import java.net.HttpURLConnection +import java.net.URI +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption + +class GitHubClient(private val token: String?) { + private val mapper = jacksonObjectMapper() + + fun fetchLatestRelease(plugin: UpdatablePlugin): Map? = runCatching { + val connection = openConnection(plugin.latestReleaseUrl, "application/vnd.github+json") + connection.setRequestProperty("X-GitHub-Api-Version", "2022-11-28") + connection.connect() + + if (connection.responseCode != 200) { + return null + } + + connection.inputStream.use { input -> + mapper.readValue(input, object : TypeReference>() {}) + } + }.getOrNull() + + fun downloadAsset(url: String, target: Path) { + val connection = openConnection(url, "application/octet-stream") + connection.connect() + + connection.inputStream.use { input -> + Files.copy(input, target, StandardCopyOption.REPLACE_EXISTING) + } + } + + private fun openConnection(url: String, accept: String): HttpURLConnection { + val connection = URI(url).toURL().openConnection() as HttpURLConnection + connection.setRequestProperty("Accept", accept) + connection.setRequestProperty("User-Agent", "surf-core-launcher") + connection.connectTimeout = 10_000 + connection.readTimeout = 20_000 + token?.let { connection.setRequestProperty("Authorization", "Bearer $it") } + return connection + } +} \ No newline at end of file diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginScanner.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginScanner.kt new file mode 100644 index 00000000..b879ceea --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginScanner.kt @@ -0,0 +1,67 @@ +package dev.slne.surf.core.launcher.server.updater.process + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import dev.slne.surf.core.launcher.server.CoreLauncher +import dev.slne.surf.core.launcher.server.updater.UpdatablePlugin +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.yaml.snakeyaml.Yaml +import java.nio.file.Files +import java.nio.file.Path +import java.util.jar.JarFile +import kotlin.io.path.extension +import kotlin.io.path.name + +class PluginScanner(private val pluginsPath: Path) { + private val yaml = Yaml() + + suspend fun findPlugins(): List = withContext(Dispatchers.IO) { + if (!Files.exists(pluginsPath)) { + return@withContext emptyList() + } + + Files.list(pluginsPath).use { stream -> + stream + .filter { it.extension == "jar" } + .filter { it.name.startsWith("surf-") } + .toList() + .mapNotNull { readPlugin(it) } + .filter { it.name !in CoreLauncher.config.autoUpdateIgnoredPlugins } + .toList() + } + } + + private fun readPlugin(jarPath: Path): UpdatablePlugin? = runCatching { + JarFile(jarPath.toFile()).use { jar -> + val entry = jar.entries().asSequence().firstOrNull { + it.name == "velocity-plugin.json" || + it.name == "paper-plugin.yml" || + it.name == "plugin.yml" + } ?: return null + + jar.getInputStream(entry).use { input -> + val data: Map = when (entry.name) { + "velocity-plugin.json" -> jacksonObjectMapper().readValue( + input, + Map::class.java + ) as Map + + else -> yaml.load(input) ?: return null + } + + val version = data["version"]?.toString() ?: return null + + val name = when (entry.name) { + "velocity-plugin.json" -> data["id"]?.toString() + else -> data["name"]?.toString() + } ?: return null + + UpdatablePlugin( + name = name, + currentVersion = version, + jarPath = jarPath + ) + } + } + }.getOrNull() +} \ No newline at end of file diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginUpdater.kt b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginUpdater.kt new file mode 100644 index 00000000..8ccf89e3 --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-server/src/main/kotlin/dev/slne/surf/core/launcher/server/updater/process/PluginUpdater.kt @@ -0,0 +1,138 @@ +package dev.slne.surf.core.launcher.server.updater.process + +import dev.slne.surf.core.launcher.server.CoreLauncher +import dev.slne.surf.core.launcher.server.LOG_PREFIX +import dev.slne.surf.core.launcher.server.updater.UpdatablePlugin +import dev.slne.surf.core.launcher.server.updater.cooldown.UpdateCooldownTracker +import dev.slne.surf.core.launcher.server.updater.github.GitHubClient +import kotlinx.coroutines.* +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption +import kotlin.system.measureTimeMillis + +object PluginUpdater { + private val pluginsPath = Path.of("plugins") + private val oldPath = pluginsPath.resolve(".old") + + private val scanner = PluginScanner(pluginsPath) + private val gitHubClient = GitHubClient(CoreLauncher.config.personalAccessToken) + private val cooldownTracker = UpdateCooldownTracker(pluginsPath.resolve(".last-updates")) + + suspend fun start() { + val plugins = scanner.findPlugins() + + if (plugins.isEmpty()) { + println("$LOG_PREFIX (Updater) No plugins found for update checking.") + return + } + + withContext(Dispatchers.IO) { + if (!Files.exists(oldPath)) { + Files.createDirectories(oldPath) + } + } + + println("$LOG_PREFIX (Updater) Checking plugin updates for ${plugins.size} plugins...") + + val duration = measureTimeMillis { + coroutineScope { + plugins.map { plugin -> + async(Dispatchers.IO) { + checkAndUpdate(plugin) + } + }.awaitAll() + } + } + + println("$LOG_PREFIX (Updater) Update check completed in ${duration}ms") + } + + private suspend fun checkAndUpdate(plugin: UpdatablePlugin) = withContext(Dispatchers.IO) { + if (cooldownTracker.isOnCooldown(plugin.name)) { + return@withContext + } + + val release = gitHubClient.fetchLatestRelease(plugin) ?: return@withContext + + val latestVersion = release["tag_name"]?.toString()?.trimStart('v') ?: return@withContext + + if (!isNewer(latestVersion, plugin.currentVersion)) { + return@withContext + } + + @Suppress("UNCHECKED_CAST") + val assets = release["assets"] as? List> ?: return@withContext + + val expectedPrefix = buildString { + append("surf-") + append(plugin.findPluginName()) + + plugin.findPluginType()?.let { + append("-") + append(plugin.findPluginType()) + } + } + + val matchingAsset = assets.firstOrNull { asset -> + val assetName = asset["name"]?.toString() ?: return@firstOrNull false + + assetName.endsWith(".jar") && + assetName.startsWith(expectedPrefix) + } ?: run { + println("$LOG_PREFIX (Updater) No matching asset found for ${plugin.name} in release $latestVersion") + return@withContext + } + + val downloadUrl = matchingAsset["browser_download_url"]?.toString() ?: return@withContext + val assetName = matchingAsset["name"]?.toString() ?: return@withContext + + backupOldJar(plugin) + + gitHubClient.downloadAsset(downloadUrl, pluginsPath.resolve(assetName)) + cooldownTracker.markUpdated(plugin.name) + + println("$LOG_PREFIX (Updater) Updated ${plugin.name} from ${plugin.currentVersion} to $latestVersion") + } + + private fun backupOldJar(plugin: UpdatablePlugin) { + if (!Files.exists(plugin.jarPath)) { + return + } + + Files.move( + plugin.jarPath, + oldPath.resolve("${plugin.name}-${plugin.currentVersion}.jar"), + StandardCopyOption.REPLACE_EXISTING + ) + } + + private fun isNewer(latest: String, current: String): Boolean { + fun split(v: String): Pair, String?> { + val parts = v.split("-", limit = 2) + val nums = parts[0].split(".").map { it.toIntOrNull() ?: 0 } + val suffix = parts.getOrNull(1)?.uppercase() + return nums to suffix + } + + fun rank(suffix: String?): Int { + if (suffix == null) return 3 + return when { + suffix.contains("SNAPSHOT") -> 0 + suffix.contains("ALPHA") -> 1 + suffix.contains("BETA") -> 2 + else -> 1 + } + } + + val (lNums, lSuffix) = split(latest) + val (cNums, cSuffix) = split(current) + + for (i in 0 until maxOf(lNums.size, cNums.size)) { + val diff = lNums.getOrElse(i) { 0 } - cNums.getOrElse(i) { 0 } + if (diff != 0) return diff > 0 + } + + return rank(lSuffix) > rank(cSuffix) + } +} \ No newline at end of file diff --git a/surf-core-launcher/surf-core-launcher-server/src/main/resources/logging.properties b/surf-core-launcher/surf-core-launcher-server/src/main/resources/logging.properties new file mode 100644 index 00000000..f9533261 --- /dev/null +++ b/surf-core-launcher/surf-core-launcher-server/src/main/resources/logging.properties @@ -0,0 +1,5 @@ +handlers=java.util.logging.ConsoleHandler +.level=INFO +java.util.logging.ConsoleHandler.level=ALL +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format=\u001B[0;91m[CoreLauncher]\u001B[0m %4$s: %5$s%n \ No newline at end of file diff --git a/surf-core-paper/build.gradle.kts b/surf-core-paper/build.gradle.kts index ef5256b3..542fc08b 100644 --- a/surf-core-paper/build.gradle.kts +++ b/surf-core-paper/build.gradle.kts @@ -27,4 +27,5 @@ surfPaperPluginApi { dependencies { api(projects.surfCoreCore.surfCoreCorePaper) compileOnly("net.luckperms:api:5.4") + implementation(projects.surfCoreLauncher.surfCoreLauncherApi) } \ No newline at end of file diff --git a/surf-core-paper/src/main/kotlin/dev/slne/surf/core/paper/PaperBootstrap.kt b/surf-core-paper/src/main/kotlin/dev/slne/surf/core/paper/PaperBootstrap.kt index 3dbca9c0..1f9fb986 100644 --- a/surf-core-paper/src/main/kotlin/dev/slne/surf/core/paper/PaperBootstrap.kt +++ b/surf-core-paper/src/main/kotlin/dev/slne/surf/core/paper/PaperBootstrap.kt @@ -9,6 +9,7 @@ import dev.slne.surf.core.core.CoreInstance import dev.slne.surf.core.core.common.config.SurfServerConfiguration import dev.slne.surf.core.core.common.event.SurfEventBus import dev.slne.surf.core.core.common.server.SurfServerService +import dev.slne.surf.core.launcher.api.LauncherConstants import dev.slne.surf.core.paper.api.DefaultCorePlayerInfoProvider import dev.slne.surf.core.paper.redis.listener.PaperRedisListener import dev.slne.surf.core.paper.teleport.TeleportRedisListener @@ -46,9 +47,12 @@ class PaperBootstrap : PluginBootstrap { startedAt = OffsetDateTime.now() ) - SurfEventBus.fire(SurfServerStartEvent(surfServerConfig.serverName)) - SurfServerService.addServer(server) + if (System.getProperty(LauncherConstants.PROPERTY_LAUNCHED_BY_CORE) == null) { + SurfEventBus.fire(SurfServerStartEvent(surfServerConfig.serverName)) + } + + SurfServerService.addServer(server) CorePlayerInfoProvider.setInstance(DefaultCorePlayerInfoProvider) } diff --git a/surf-core-paper/src/main/kotlin/dev/slne/surf/core/paper/command/SurfCoreCommand.kt b/surf-core-paper/src/main/kotlin/dev/slne/surf/core/paper/command/SurfCoreCommand.kt index 76a757e0..04efcf41 100644 --- a/surf-core-paper/src/main/kotlin/dev/slne/surf/core/paper/command/SurfCoreCommand.kt +++ b/surf-core-paper/src/main/kotlin/dev/slne/surf/core/paper/command/SurfCoreCommand.kt @@ -48,6 +48,38 @@ fun surfCoreCommand() = commandTree("surfcore") { } } +// literalArgument("crash") { +// withRequirement { +// SurfServer.current().name.contains("dev") +// } +// +// anyExecutor { executor, _ -> +// executor.sendText { +// appendCorePrefix() +// error("Der Server wird nun absichtlich zum Testen von Crash-Handling-Funktionen abstürzen...") +// } +// +// Runtime.getRuntime().halt(1) +// } +// } +// +// literalArgument("unreachable") { +// withRequirement { +// SurfServer.current().name.contains("dev") +// } +// +// anyExecutor { executor, _ -> +// executor.sendText { +// appendCorePrefix() +// error("Der Server wird nun absichtlich unerreichbar gemacht, um die Handhabung von Verbindungsproblemen zu testen... o7") +// } +// +// while (true) { +// Thread.sleep(1000) +// } +// } +// } + literalArgument("testawaitingsend") { literalArgument("server") { surfBackendServerArgument("backend") { diff --git a/surf-core-velocity/build.gradle.kts b/surf-core-velocity/build.gradle.kts index 25f15c05..7683b16c 100644 --- a/surf-core-velocity/build.gradle.kts +++ b/surf-core-velocity/build.gradle.kts @@ -17,4 +17,5 @@ velocityPluginFile { dependencies { api(projects.surfCoreCore.surfCoreCoreVelocity) + implementation(projects.surfCoreLauncher.surfCoreLauncherApi) } \ No newline at end of file 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..246bf93f 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 @@ -25,6 +25,7 @@ import dev.slne.surf.core.core.common.event.SurfEventBus import dev.slne.surf.core.core.common.server.SurfServerService import dev.slne.surf.core.core.common.util.appendCorePrefix import dev.slne.surf.core.core.common.util.niceRed +import dev.slne.surf.core.launcher.api.LauncherConstants import dev.slne.surf.core.velocity.auth.AuthenticationListener import dev.slne.surf.core.velocity.auth.AuthenticationService import dev.slne.surf.core.velocity.command.coreCommand @@ -83,7 +84,10 @@ class VelocityMain @Inject constructor( ) ) - SurfEventBus.fire(SurfServerStartEvent(surfServerConfig.serverName)) + if (System.getProperty(LauncherConstants.PROPERTY_LAUNCHED_BY_CORE) == null) { + SurfEventBus.fire(SurfServerStartEvent(surfServerConfig.serverName)) + } + SurfServerService.addServer(server) } diff --git a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/command/CoreCommand.kt b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/command/CoreCommand.kt index f6c1fa6f..1273ae8f 100644 --- a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/command/CoreCommand.kt +++ b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/command/CoreCommand.kt @@ -20,8 +20,10 @@ import dev.slne.surf.core.core.common.player.SurfPlayerService import dev.slne.surf.core.core.common.server.SurfServerService import dev.slne.surf.core.core.common.util.appendCorePrefix import dev.slne.surf.core.core.common.util.niceRed +import dev.slne.surf.core.launcher.api.LauncherConstants import dev.slne.surf.core.velocity.permission.PermissionList import dev.slne.surf.core.velocity.plugin +import dev.slne.surf.core.velocity.redis.listener.VelocityRedisListener import net.kyori.adventure.text.Component import net.kyori.adventure.text.event.ClickEvent import net.kyori.adventure.text.format.TextDecoration @@ -52,6 +54,33 @@ fun coreCommand() = commandTree("core") { info(" by ") variableValue(vendor) info(".") + + if (System.getProperty(LauncherConstants.PROPERTY_LAUNCHED_BY_CORE) != null) { + spacer(" (and is launched by the core launcher)") + } + } + } + + literalArgument("togglecoreservicestatusmessages") { + withPermission(PermissionList.CORE_COMMAND_TOGGLE_SERVICE_STATUS_MESSAGES) + + playerExecutor { player, _ -> + val current = VelocityRedisListener.ignoringPlayers.contains(player.uniqueId) + + if (current) { + VelocityRedisListener.ignoringPlayers.remove(player.uniqueId) + } else { + VelocityRedisListener.ignoringPlayers.add(player.uniqueId) + } + + player.sendText { + appendCorePrefix() + if (current) { + success("Du erhältst nun wieder Service Statusnachrichten.") + } else { + success("Du erhältst nun keine Service Statusnachrichten mehr.") + } + } } } diff --git a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/permission/PermissionList.kt b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/permission/PermissionList.kt index 6e1f826a..4f5637c8 100644 --- a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/permission/PermissionList.kt +++ b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/permission/PermissionList.kt @@ -4,9 +4,14 @@ object PermissionList { private const val BASE = "surf.core" const val CORE_COMMAND = "$BASE.command" + const val CORE_COMMAND_PLAYER = "$CORE_COMMAND.player" + const val CORE_COMMAND_SERVICE = "$CORE_COMMAND.service" + const val CORE_COMMAND_TOGGLE_SERVICE_STATUS_MESSAGES = + "$CORE_COMMAND.togglecoreservicestatusmessages" + const val BYPASS_PERMISSION = "$BASE.bypass" const val TEAM_PERMISSION = "$BASE.team" } \ No newline at end of file diff --git a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/redis/listener/VelocityRedisListener.kt b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/redis/listener/VelocityRedisListener.kt index bc2da92b..44f1822a 100644 --- a/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/redis/listener/VelocityRedisListener.kt +++ b/surf-core-velocity/src/main/kotlin/dev/slne/surf/core/velocity/redis/listener/VelocityRedisListener.kt @@ -1,11 +1,21 @@ package dev.slne.surf.core.velocity.redis.listener +import com.github.benmanes.caffeine.cache.Caffeine +import com.sksamuel.aedile.core.expireAfterWrite +import dev.slne.surf.api.core.messages.adventure.sendText +import dev.slne.surf.api.core.util.random +import dev.slne.surf.core.api.common.server.state.SurfServiceStatus import dev.slne.surf.core.core.common.redis.event.SurfPlayerMessageRedisEvent import dev.slne.surf.core.core.common.redis.event.SurfPlayerResyncRedisEvent +import dev.slne.surf.core.core.common.util.appendCorePrefix +import dev.slne.surf.core.launcher.api.redis.ServiceStatusRedisEvent import dev.slne.surf.core.velocity.plugin import dev.slne.surf.core.velocity.task.surfPlayerSyncTask import dev.slne.surf.redis.event.OnRedisEvent +import java.util.* +import java.util.concurrent.ConcurrentHashMap import kotlin.jvm.optionals.getOrNull +import kotlin.time.Duration.Companion.seconds object VelocityRedisListener { @OnRedisEvent @@ -17,4 +27,53 @@ object VelocityRedisListener { fun onSurfPlayerResync(event: SurfPlayerResyncRedisEvent) { surfPlayerSyncTask.syncPlayers() } + + private val instableConnectionCache = Caffeine.newBuilder() + .expireAfterWrite(10.seconds) + .build() + + val ignoringPlayers: ConcurrentHashMap.KeySetView = ConcurrentHashMap.newKeySet() + + @OnRedisEvent + fun onServiceStatus(event: ServiceStatusRedisEvent) { + val status = event.status + val cachedAmount = instableConnectionCache.asMap().values.count { + it.status == status && it.serviceName == event.serviceName + } + if (cachedAmount < 2) { + plugin.proxy.allPlayers.filter { it.hasPermission("surf.core.servernotify") }.forEach { + if (ignoringPlayers.contains(it.uniqueId)) { + return@forEach + } + + it.sendText { + appendCorePrefix() + error("Der Server ") + variableValue(event.serviceName) + + when (status) { + SurfServiceStatus.UNREACHABLE -> error(" hat derzeit Verbindungsprobleme!") + SurfServiceStatus.CRASHED -> { + error(" hat die Verbindung verloren! (CRASH?)") + } + } + } + } + } else { + plugin.proxy.allPlayers.filter { it.hasPermission("surf.core.servernotify") }.forEach { + if (ignoringPlayers.contains(it.uniqueId)) { + return@forEach + } + + it.sendText { + appendCorePrefix() + error("Der Server ") + variableValue(event.serviceName) + error(" hat derzeit Verbindungsprobleme! Check server logs. (x${cachedAmount + 1} $status)") + } + } + } + + instableConnectionCache.put(random.nextInt(), event) + } } \ No newline at end of file