diff --git a/common/src/main/kotlin/com/bluedragonmc/server/Game.kt b/common/src/main/kotlin/com/bluedragonmc/server/Game.kt index c83078d..b152ea4 100644 --- a/common/src/main/kotlin/com/bluedragonmc/server/Game.kt +++ b/common/src/main/kotlin/com/bluedragonmc/server/Game.kt @@ -41,7 +41,6 @@ import net.minestom.server.event.server.ServerTickMonitorEvent import net.minestom.server.event.trait.InstanceEvent import net.minestom.server.event.trait.PlayerEvent import net.minestom.server.instance.Instance -import net.minestom.server.tag.Tag import net.minestom.server.timer.ExecutionType import net.minestom.server.utils.async.AsyncUtils import org.slf4j.Logger @@ -234,9 +233,8 @@ abstract class Game(val name: String, val mapName: String, val mode: String? = n }.delay(delay).schedule() } - open fun endGame(queueAllPlayers: Boolean = true) { - - // Log some information about the game in the database + private fun logGameInfo() { + if (!::startTime.isInitialized) return val statHistory = getModuleOrNull()?.getHistory() val teams = getModuleOrNull()?.teams?.map { team -> @@ -269,24 +267,27 @@ abstract class Game(val name: String, val mapName: String, val mode: String? = n } Database.IO.launch { - if (::startTime.isInitialized) { - Database.connection.logGame( - GameDocument( - gameId = id, - serverId = Environment.getServerName(), - gameType = name, - mapName = mapName, - mode = mode, - statistics = statHistory, - teams = teams, - winningTeam = winningTeamRecord, - startTime = startTime, - endTime = Date(), - instances = instanceRecords - ) + Database.connection.logGame( + GameDocument( + gameId = id, + serverId = Environment.getServerName(), + gameType = name, + mapName = mapName, + mode = mode, + statistics = statHistory, + teams = teams, + winningTeam = winningTeamRecord, + startTime = startTime, + endTime = Date(), + instances = instanceRecords ) - } + ) } + } + + open fun endGame(queueAllPlayers: Boolean = true) { + + logGameInfo() state = GameState.ENDING games.remove(this) @@ -312,8 +313,10 @@ abstract class Game(val name: String, val mapName: String, val mode: String? = n MinecraftServer.getSchedulerManager().buildTask { instancesToRemove.forEach { instance -> - logger.info("Forcefully unregistering instance ${instance.uniqueId}...") - InstanceUtils.forceUnregisterInstance(instance) + if (instance.isRegistered) { + logger.info("Forcefully unregistering instance ${instance.uniqueId}...") + InstanceUtils.forceUnregisterInstance(instance) + } } }.executionType(ExecutionType.TICK_START).delay(Duration.ofSeconds(10)) @@ -375,13 +378,6 @@ abstract class Game(val name: String, val mapName: String, val mode: String? = n */ private val INSTANCE_CLEANUP_PERIOD = System.getenv("SERVER_INSTANCE_CLEANUP_PERIOD")?.toLongOrNull() ?: 10_000L - /** - * Instances must be inactive for at least 2 minutes - * to be cleaned up (by default). - */ - private val CLEANUP_MIN_INACTIVE_TIME = - System.getenv("SERVER_INSTANCE_MIN_INACTIVE_TIME")?.toLongOrNull() ?: 120_000L - fun findGame(player: Player): Game? = games.find { player in it.players || it.ownsInstance(player.instance ?: return@find false) } @@ -392,8 +388,6 @@ abstract class Game(val name: String, val mapName: String, val mode: String? = n fun findGame(gameId: String): Game? = games.find { it.id == gameId } - private val INACTIVE_SINCE_TAG = Tag.Long("instance_inactive_since") - init { MinecraftServer.getSchedulerManager().buildShutdownTask { ArrayList(games).forEach { game -> @@ -403,37 +397,21 @@ abstract class Game(val name: String, val mapName: String, val mode: String? = n timer("Cleanup", daemon = true, period = INSTANCE_CLEANUP_PERIOD) { val instances = MinecraftServer.getInstanceManager().instances - val games = ArrayList(games) // Copy to avoid CME - - instances.forEach { instance -> - val owner = findGame(instance.uniqueId) - // Remove empty instances which are not owned by a game OR are owned by an inactive game. - if ((owner == null || owner.isInactive()) && instance.players.isEmpty()) { - // Only instances that are not required by any game should be removed. - if (games.none { it.getRequiredInstances().contains(instance) }) { - val inactiveSince = instance.getTag(INACTIVE_SINCE_TAG) - if (inactiveSince == null) { - // The instance has recently turned inactive. - instance.setTag(INACTIVE_SINCE_TAG, System.currentTimeMillis()) - } else { - val duration = System.currentTimeMillis() - inactiveSince - if (duration >= CLEANUP_MIN_INACTIVE_TIME) { - // Instances inactive for more than the minimum inactive time should be removed. - logger.info("Removing inactive instance ${instance.uniqueId}") - InstanceUtils.forceUnregisterInstance(instance) - } - } - } - } else instance.removeTag(INACTIVE_SINCE_TAG) - } - // End all inactive games with no owned instances games.forEach { game -> - if (game.isInactive() && game.getOwnedInstances().isEmpty()) { - logger.info("Removing inactive game ${game.id} (${game.name}/${game.mapName}/${game.mode})") + if (game.isInactive()) { + logger.info("Ending inactive game ${game.id} (${game.name}/${game.mapName}/${game.mode})") game.endGame(false) } } + + instances.forEach { instance -> + val owner = games.find { it.ownsInstance(instance) } + if (owner == null && games.none { it.getRequiredInstances().contains(instance) }) { + logger.info("Removing orphan instance ${instance.uniqueId} (${instance})") + InstanceUtils.forceUnregisterInstance(instance) + } + } } } } diff --git a/common/src/main/kotlin/com/bluedragonmc/server/module/map/AnvilFileMapProviderModule.kt b/common/src/main/kotlin/com/bluedragonmc/server/module/map/AnvilFileMapProviderModule.kt index b5ca0a0..228e1e0 100644 --- a/common/src/main/kotlin/com/bluedragonmc/server/module/map/AnvilFileMapProviderModule.kt +++ b/common/src/main/kotlin/com/bluedragonmc/server/module/map/AnvilFileMapProviderModule.kt @@ -2,16 +2,17 @@ package com.bluedragonmc.server.module.map import com.bluedragonmc.server.Game import com.bluedragonmc.server.module.GameModule -import com.bluedragonmc.server.utils.InstanceUtils import net.minestom.server.MinecraftServer import net.minestom.server.event.Event import net.minestom.server.event.EventNode -import net.minestom.server.instance.* +import net.minestom.server.event.instance.InstanceUnregisterEvent +import net.minestom.server.instance.DynamicChunk +import net.minestom.server.instance.InstanceContainer +import net.minestom.server.instance.LightingChunk import net.minestom.server.instance.anvil.AnvilLoader import net.minestom.server.registry.DynamicRegistry import net.minestom.server.tag.Tag import net.minestom.server.world.DimensionType -import org.slf4j.LoggerFactory import java.nio.file.Path import kotlin.io.path.absolutePathString @@ -50,28 +51,13 @@ class AnvilFileMapProviderModule(val worldFolder: Path, private val dimensionTyp } companion object { - - private val logger = LoggerFactory.getLogger(Companion::class.java) - val loadedMaps = mutableMapOf() - val MAP_NAME_TAG = Tag.String("anvil_file_map_name") - /** - * This method should be called when a SharedInstance is unregistered. - * It will check if the unregistrered instance is the last instance - * which depends on an Anvil map to be loaded, and if so, unloads the map. - */ - fun checkReleaseMap(instance: Instance) { - if (instance !is SharedInstance) return - val isLast = MinecraftServer.getInstanceManager().instances.none { - it !== instance && it is SharedInstance && it.instanceContainer === instance.instanceContainer - } - if (isLast) { - val key = loadedMaps.entries.find { it.value === instance.instanceContainer }?.key - loadedMaps.remove(key) - InstanceUtils.forceUnregisterInstance(instance.instanceContainer) - logger.info("Map file '${key?.fileName}' has been unloaded and its instance container has been unregistered.") + init { + // If an InstanceContainer is unregistered, remove it from `loadedMaps` so it can be garbage collected + MinecraftServer.getGlobalEventHandler().addListener(InstanceUnregisterEvent::class.java) { event -> + loadedMaps.entries.removeIf { (_, instance) -> instance == event.instance } } } } diff --git a/common/src/main/kotlin/com/bluedragonmc/server/utils/InstanceUtils.kt b/common/src/main/kotlin/com/bluedragonmc/server/utils/InstanceUtils.kt index c024fa1..7b17ec4 100644 --- a/common/src/main/kotlin/com/bluedragonmc/server/utils/InstanceUtils.kt +++ b/common/src/main/kotlin/com/bluedragonmc/server/utils/InstanceUtils.kt @@ -24,10 +24,12 @@ object InstanceUtils { * @return a CompletableFuture when all players are removed and the instance is unregistered. */ fun forceUnregisterInstance(instance: Instance): CompletableFuture { - val eventNode = EventNode.all("temp-${UUID.randomUUID()}") + val eventNode = EventNode.all("temp-vacate-${UUID.randomUUID()}") eventNode.addListener(PlayerEvent::class.java) { event -> - event.player.kick(Component.text("This instance is shutting down.")) - event.player.remove() + if (event.player.instance == instance) { + event.player.kick(Component.text("This instance is shutting down.")) + event.player.remove() + } } MinecraftServer.getGlobalEventHandler().addChild(eventNode) return vacateInstance(instance).thenRun { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fa79ff9..e38573f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,7 @@ okhttp = "4.10.0" serialization = "1.5.0-RC" tinylog = "2.6.2" # Auto-generated GRPC/Protobuf messaging code -rpc = "18b740c038" +rpc = "dfd52fb7cc" # Agones SDK and its necessary runtime dependencies agones-kt = "0.1.2" grpc = "1.50.2" diff --git a/src/main/kotlin/com/bluedragonmc/server/bootstrap/prod/InitialInstanceRouter.kt b/src/main/kotlin/com/bluedragonmc/server/bootstrap/prod/InitialInstanceRouter.kt index 9561792..c35488b 100644 --- a/src/main/kotlin/com/bluedragonmc/server/bootstrap/prod/InitialInstanceRouter.kt +++ b/src/main/kotlin/com/bluedragonmc/server/bootstrap/prod/InitialInstanceRouter.kt @@ -22,6 +22,8 @@ object InitialInstanceRouter : Bootstrap(EnvType.PRODUCTION) { private val INVALID_WORLD = Component.text("Couldn't find which world to put you in! (Invalid world name)", NamedTextColor.RED) + private val INSTANCE_NOT_REGISTERED = + Component.text("Couldn't find which world to put you in! (Destination not ready)", NamedTextColor.RED) private val HANDSHAKE_FAILED = Component.text("Couldn't find which world to put you in! (Handshake failed)", NamedTextColor.RED) private val DATA_LOAD_FAILED = @@ -81,6 +83,12 @@ object InitialInstanceRouter : Bootstrap(EnvType.PRODUCTION) { return@listenSuspend } + if (!instance.isRegistered) { + logger.warn("Tried to send ${event.player.username} to an unregistered instance!") + event.player.kick(INSTANCE_NOT_REGISTERED) + return@listenSuspend + } + logger.info("Spawning player ${event.player.username} in game '${game.id}' and instance '${instance.uniqueId}'") event.spawningInstance = instance diff --git a/src/main/kotlin/com/bluedragonmc/server/impl/IncomingRPCHandlerImpl.kt b/src/main/kotlin/com/bluedragonmc/server/impl/IncomingRPCHandlerImpl.kt index 81cc3a2..0bb2c6c 100644 --- a/src/main/kotlin/com/bluedragonmc/server/impl/IncomingRPCHandlerImpl.kt +++ b/src/main/kotlin/com/bluedragonmc/server/impl/IncomingRPCHandlerImpl.kt @@ -7,7 +7,10 @@ import com.bluedragonmc.server.api.IncomingRPCHandler import com.bluedragonmc.server.utils.miniMessage import com.google.protobuf.Empty import io.grpc.ServerBuilder +import net.kyori.adventure.sound.Sound +import net.kyori.adventure.sound.SoundStop import net.minestom.server.MinecraftServer +import net.minestom.server.utils.NamespaceID import org.slf4j.LoggerFactory import java.util.* import java.util.concurrent.TimeUnit @@ -53,6 +56,7 @@ class IncomingRPCHandlerImpl(serverPort: Int) : IncomingRPCHandler { gameState = CommonTypes.EnumGameState.ERROR joinable = false openSlots = 0 + maxSlots = 0 } return createInstanceResponse { this.gameState = state @@ -76,6 +80,26 @@ class IncomingRPCHandlerImpl(serverPort: Int) : IncomingRPCHandler { return Empty.getDefaultInstance() } + override suspend fun playSound(request: GsClient.PlaySoundRequest): Empty { + val target = + MinecraftServer.getConnectionManager().getOnlinePlayerByUuid(UUID.fromString(request.playerUuid)) + ?: return Empty.getDefaultInstance() + + target.playSound(Sound.sound(NamespaceID.from(request.soundId), Sound.Source.valueOf(request.category), request.volume, request.pitch)) + + return Empty.getDefaultInstance() + } + + override suspend fun stopSound(request: GsClient.StopSoundRequest): Empty { + val target = + MinecraftServer.getConnectionManager().getOnlinePlayerByUuid(UUID.fromString(request.playerUuid)) + ?: return Empty.getDefaultInstance() + + target.stopSound(SoundStop.namedOnSource(NamespaceID.from(request.soundId), Sound.Source.valueOf(request.category))) + + return Empty.getDefaultInstance() + } + override suspend fun getInstances(request: Empty): GsClient.GetInstancesResponse { return getInstancesResponse { Game.games.forEach { game -> @@ -90,6 +114,12 @@ class IncomingRPCHandlerImpl(serverPort: Int) : IncomingRPCHandler { } } } + + override suspend fun endGame(request: GsClient.EndGameRequest): Empty { + val game = Game.findGame(request.gameId) + game?.endGame(request.queuePlayersForLobby) + return Empty.getDefaultInstance() + } } class PlayerHolderService : PlayerHolderGrpcKt.PlayerHolderCoroutineImplBase() {