From a40dbfbc1cdde8674dec5b216c855cc0d8e6f362 Mon Sep 17 00:00:00 2001 From: Ceikry Date: Tue, 24 May 2022 11:02:05 +0000 Subject: [PATCH] Improved server shutdown order Improved GE threading, introduced locks fixing server lags and GE offers not executing Moved player login hooks to the major update worker (from management server thread) Fixed random events in rare circumstances causing noted item loss --- .../core/game/node/entity/player/Player.java | 4 +- .../core/game/system/SystemTermination.java | 6 +++ .../main/kotlin/rs09/game/ai/AIRepository.kt | 4 ++ .../rs09/game/content/ame/RandomEventNPC.kt | 2 +- Server/src/main/kotlin/rs09/game/ge/GEDB.kt | 13 ++++++ .../main/kotlin/rs09/game/ge/GrandExchange.kt | 29 +++++++------- .../main/kotlin/rs09/game/ge/PriceIndex.kt | 2 + .../entity/player/info/login/LoginParser.kt | 7 ++-- .../rottenpotato/RottenPotatoRSHDDialogue.kt | 2 +- .../kotlin/rs09/worker/MajorUpdateWorker.kt | 12 ++++-- Server/src/test/kotlin/ExchangeTests.kt | 15 +++++++ .../test/kotlin/content/RandomEventManager.kt | 40 +++++++++++++++++++ 12 files changed, 112 insertions(+), 24 deletions(-) diff --git a/Server/src/main/java/core/game/node/entity/player/Player.java b/Server/src/main/java/core/game/node/entity/player/Player.java index 2c8add884..141f378e1 100644 --- a/Server/src/main/java/core/game/node/entity/player/Player.java +++ b/Server/src/main/java/core/game/node/entity/player/Player.java @@ -368,10 +368,10 @@ public class Player extends Entity { if (force) { Repository.getDisconnectionQueue().remove(getName()); } - GameWorld.getLogoutListeners().forEach((it) -> it.logout(this)); + if (!isArtificial()) + GameWorld.getLogoutListeners().forEach((it) -> it.logout(this)); setPlaying(false); getWalkingQueue().reset(); - GameWorld.getLogoutListeners().forEach((it) -> it.logout(this)); if(!logoutListeners.isEmpty()){ logoutListeners.forEach((key,method) -> method.invoke(this)); } diff --git a/Server/src/main/java/core/game/system/SystemTermination.java b/Server/src/main/java/core/game/system/SystemTermination.java index c21f9bcfa..f0e2a3abc 100644 --- a/Server/src/main/java/core/game/system/SystemTermination.java +++ b/Server/src/main/java/core/game/system/SystemTermination.java @@ -5,6 +5,7 @@ import api.ShutdownListener; import core.game.node.entity.player.Player; import rs09.Server; import rs09.ServerConstants; +import rs09.game.ai.AIRepository; import rs09.game.system.SystemLogger; import rs09.game.world.GameWorld; import rs09.game.world.repository.Repository; @@ -34,8 +35,13 @@ public final class SystemTermination { public void terminate() { SystemLogger.logInfo("[SystemTerminator] Initializing termination sequence - do not shutdown!"); try { + SystemLogger.logInfo("[SystemTerminator] Stopping all bots..."); + AIRepository.clearAllBots(); + SystemLogger.logInfo("[SystemTerminator] Shutting down networking..."); Server.setRunning(false); Server.getReactor().terminate(); + SystemLogger.logInfo("[SystemTerminator] Stopping all pulses..."); + GameWorld.getMajorUpdateWorker().stop(); for (Iterator it = Repository.getPlayers().iterator(); it.hasNext();) { try { Player p = it.next(); diff --git a/Server/src/main/kotlin/rs09/game/ai/AIRepository.kt b/Server/src/main/kotlin/rs09/game/ai/AIRepository.kt index 8334ff3ee..52f5840d8 100644 --- a/Server/src/main/kotlin/rs09/game/ai/AIRepository.kt +++ b/Server/src/main/kotlin/rs09/game/ai/AIRepository.kt @@ -42,5 +42,9 @@ class AIRepository { fun getOffer(player: Player): GrandExchangeOffer? { return GEOffers[player] } + + @JvmStatic fun clearAllBots() { + PulseRepository.toTypedArray().forEach { it.stop(); it.botScript.bot.clear(); AIPlayer.deregister((it.botScript.bot as AIPlayer).uid) } + } } } \ No newline at end of file diff --git a/Server/src/main/kotlin/rs09/game/content/ame/RandomEventNPC.kt b/Server/src/main/kotlin/rs09/game/content/ame/RandomEventNPC.kt index 0d17b92e2..234a06c98 100644 --- a/Server/src/main/kotlin/rs09/game/content/ame/RandomEventNPC.kt +++ b/Server/src/main/kotlin/rs09/game/content/ame/RandomEventNPC.kt @@ -92,7 +92,7 @@ abstract class RandomEventNPC(id: Int) : NPC(id) { fun noteAndTeleport() { for (item in player.inventory.toArray()) { if (item == null) continue - if (item.noteChange != item.id && item.noteChange != -1) { + if (item.definition.isUnnoted) { player.inventory.remove(item) player.inventory.add(Item(item.noteChange, item.amount)) } diff --git a/Server/src/main/kotlin/rs09/game/ge/GEDB.kt b/Server/src/main/kotlin/rs09/game/ge/GEDB.kt index 0571ae7f8..3bfd7631a 100644 --- a/Server/src/main/kotlin/rs09/game/ge/GEDB.kt +++ b/Server/src/main/kotlin/rs09/game/ge/GEDB.kt @@ -10,6 +10,8 @@ import rs09.game.system.SystemLogger import java.io.File import java.io.FileReader import java.sql.Connection +import java.util.concurrent.TimeUnit +import java.util.concurrent.locks.ReentrantLock /** * Collection of methods for interacting with the grand exchange databases @@ -20,6 +22,8 @@ object GEDB { public var connection: Connection? = null private var initialized = false private var connectionRefs = 0 + private var obtainConnectionLock = ReentrantLock() + private var dbRunLock = ReentrantLock() fun init() { init(File(ServerConstants.GRAND_EXCHANGE_DATA_PATH + "grandexchange.db").absolutePath) @@ -36,23 +40,32 @@ object GEDB { } @JvmStatic fun run(closure: (conn: Connection) -> Unit) { + dbRunLock.tryLock(10000L, TimeUnit.MILLISECONDS) + connectionRefs++ val con = connect() closure.invoke(con) connectionRefs-- + if(connectionRefs == 0) { con.close() } + + dbRunLock.unlock() } private fun connect(): Connection { + obtainConnectionLock.tryLock(10000L, TimeUnit.MILLISECONDS) + if (connection == null || connection!!.isClosed) { val ds = SQLiteDataSource() ds.url = "jdbc:sqlite:$pathString" connection = ds.connection } + + obtainConnectionLock.unlock() return connection!! } diff --git a/Server/src/main/kotlin/rs09/game/ge/GrandExchange.kt b/Server/src/main/kotlin/rs09/game/ge/GrandExchange.kt index 55e7e9553..1b3831993 100644 --- a/Server/src/main/kotlin/rs09/game/ge/GrandExchange.kt +++ b/Server/src/main/kotlin/rs09/game/ge/GrandExchange.kt @@ -43,37 +43,38 @@ class GrandExchange : StartupListener, Commands { GEDB.run { conn -> val botStmt = conn.createStatement() val botOffers = botStmt.executeQuery("SELECT * from bot_offers") - while(botOffers.next()) { + while (botOffers.next()) { val bot = GrandExchangeOffer.fromBotQuery(botOffers) val buyStmt = conn.createStatement() - val buyOffer = buyStmt.executeQuery("SELECT * FROM player_offers WHERE item_id = ${bot.itemID} AND offer_state < 4 AND NOT offer_state = 2 AND offered_value >= ${bot.offeredValue}") + val buyOffer = + buyStmt.executeQuery("SELECT * FROM player_offers WHERE item_id = ${bot.itemID} AND offer_state < 4 AND NOT offer_state = 2 AND offered_value >= ${bot.offeredValue}") val buyOffers = ArrayList() - while(buyOffer.next()) buyOffers.add(GrandExchangeOffer.fromQuery(buyOffer)) + while (buyOffer.next()) buyOffers.add(GrandExchangeOffer.fromQuery(buyOffer)) buyStmt.close() - for(offer in buyOffers.sortedBy { it.offeredValue }.reversed()) { + for (offer in buyOffers.sortedBy { it.offeredValue }.reversed()) { if (bot.amountLeft <= 0) break exchange(bot, offer) } } botStmt.close() + } + val activeOffers = ArrayList() + GEDB.run { conn -> val stmt = conn.createStatement() - val activeOffer = stmt.executeQuery("SELECT * from player_offers where offer_state < 4 AND NOT offer_state = 2 AND is_sale = true") - val activeOffers = ArrayList() + val activeOffer = + stmt.executeQuery("SELECT * from player_offers where offer_state < 4 AND NOT offer_state = 2 AND is_sale = true") - while(activeOffer.next()) - { + while (activeOffer.next()) { val offer = GrandExchangeOffer.fromQuery(activeOffer) - if(!offer.isActive) continue + if (!offer.isActive) continue activeOffers.add(offer) } - - for(offer in activeOffers) - processOffer(offer) - - stmt.close() } + for(offer in activeOffers) + processOffer(offer) + Thread.sleep(15_000) //sleep for 15 seconds } }.start() diff --git a/Server/src/main/kotlin/rs09/game/ge/PriceIndex.kt b/Server/src/main/kotlin/rs09/game/ge/PriceIndex.kt index 0045dc461..94ab802e3 100644 --- a/Server/src/main/kotlin/rs09/game/ge/PriceIndex.kt +++ b/Server/src/main/kotlin/rs09/game/ge/PriceIndex.kt @@ -28,6 +28,8 @@ object PriceIndex { } fun allowItem(id: Int) { + if(canTrade(id)) return + GEDB.run { conn -> val stmt = conn.prepareStatement(INSERT_QUERY) stmt.setInt(1, id) diff --git a/Server/src/main/kotlin/rs09/game/node/entity/player/info/login/LoginParser.kt b/Server/src/main/kotlin/rs09/game/node/entity/player/info/login/LoginParser.kt index be04ff919..e7326b5f8 100644 --- a/Server/src/main/kotlin/rs09/game/node/entity/player/info/login/LoginParser.kt +++ b/Server/src/main/kotlin/rs09/game/node/entity/player/info/login/LoginParser.kt @@ -43,11 +43,10 @@ class LoginParser(val details: PlayerDetails, private val type: LoginType) { reconnect(player) return } + lateinit var parser: PlayerSaveParser try { - val parser = PlayerParser.parse(player) + parser = PlayerParser.parse(player) ?: throw IllegalStateException("Failed parsing save for: " + player.username) //Parse core - loginListeners.forEach(Consumer { listener: LoginListener -> listener.login(player) }) //Run our login hooks - parser.runContentHooks() //Run our saved-content-parsing hooks } catch (e: Exception) { @@ -74,6 +73,8 @@ class LoginParser(val details: PlayerDetails, private val type: LoginType) { if (!Repository.players.contains(player)) { Repository.addPlayer(player) } + loginListeners.forEach(Consumer { listener: LoginListener -> listener.login(player) }) //Run our login hooks + parser.runContentHooks() //Run our saved-content-parsing hooks player.details.session.setObject(player) flag(AuthResponse.Success) player.init() diff --git a/Server/src/main/kotlin/rs09/game/system/command/rottenpotato/RottenPotatoRSHDDialogue.kt b/Server/src/main/kotlin/rs09/game/system/command/rottenpotato/RottenPotatoRSHDDialogue.kt index d9d028f8c..7f27b25f5 100644 --- a/Server/src/main/kotlin/rs09/game/system/command/rottenpotato/RottenPotatoRSHDDialogue.kt +++ b/Server/src/main/kotlin/rs09/game/system/command/rottenpotato/RottenPotatoRSHDDialogue.kt @@ -35,7 +35,7 @@ class RottenPotatoRSHDDialogue(player: Player? = null) : DialoguePlugin(player) 0 -> { when (buttonId) { //Wipe Bots - 1 -> AIRepository.PulseRepository.toTypedArray().forEach { it.stop(); it.botScript.bot.clear(); AIPlayer.deregister((it.botScript.bot as AIPlayer).uid) }.also { player.sendMessage(colorize("%RBots wiped.")); end() } + 1 -> AIRepository.clearAllBots().also { player.sendMessage(colorize("%RBots wiped.")); end() } //Spawn Bots 2 -> ImmerseWorld.spawnBots().also { player.sendMessage(colorize("%RBots Respawning...")); end() } //Force Log All Online Players diff --git a/Server/src/main/kotlin/rs09/worker/MajorUpdateWorker.kt b/Server/src/main/kotlin/rs09/worker/MajorUpdateWorker.kt index b0bc22ecb..95f808039 100644 --- a/Server/src/main/kotlin/rs09/worker/MajorUpdateWorker.kt +++ b/Server/src/main/kotlin/rs09/worker/MajorUpdateWorker.kt @@ -32,6 +32,7 @@ import kotlin.system.exitProcess * @author Ceikry */ class MajorUpdateWorker { + var running: Boolean = false var started = false val sequence = UpdateSequence() val sdf = SimpleDateFormat("HHmmss") @@ -39,7 +40,7 @@ class MajorUpdateWorker { Thread.currentThread().name = "Major Update Worker" started = true Thread.sleep(600L) - while(true){ + while(running){ val start = System.currentTimeMillis() Server.heartbeat() @@ -75,6 +76,8 @@ class MajorUpdateWorker { ServerMonitor.eventQueue.add(GuiEvent.UpdatePulseCount(GameWorld.Pulser.TASKS.size))*/ Thread.sleep(max(600 - (end - start), 0)) } + + SystemLogger.logInfo("Update worker stopped.") } fun handleTickActions() { @@ -127,10 +130,13 @@ class MajorUpdateWorker { fun start() { if(!started){ + running = true worker.start() } + } - //if (ServerConstants.ALLOW_GUI) - // ServerMonitor.open() + fun stop() { + running = false + worker.interrupt() } } \ No newline at end of file diff --git a/Server/src/test/kotlin/ExchangeTests.kt b/Server/src/test/kotlin/ExchangeTests.kt index f682d8f3f..bb3e78f75 100644 --- a/Server/src/test/kotlin/ExchangeTests.kt +++ b/Server/src/test/kotlin/ExchangeTests.kt @@ -1,4 +1,7 @@ import core.game.ge.OfferState +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test @@ -7,6 +10,7 @@ import org.junit.jupiter.api.fail import rs09.game.ge.GEDB import rs09.game.ge.GrandExchange import rs09.game.ge.GrandExchangeOffer +import rs09.game.ge.PriceIndex import rs09.game.system.SystemLogger import java.io.File import kotlin.random.Random @@ -100,4 +104,15 @@ import kotlin.random.Random Assertions.assertEquals(defaultPrice, GrandExchange.getRecommendedPrice(4151)) } + + @Test fun concurrentlySubmittedOffersShouldNotThrowExceptions(){ + runBlocking { + val a = GlobalScope.launch { for(i in 0 until 5) {PriceIndex.allowItem(i); GrandExchange.addBotOffer(i, 1)} } + val b = GlobalScope.launch { for(i in 0 until 5) {PriceIndex.allowItem(i); GrandExchange.addBotOffer(i, 1)} } + a.join() + b.join() + Assertions.assertEquals(false, a.isCancelled) + Assertions.assertEquals(false, b.isCancelled) + } + } } \ No newline at end of file diff --git a/Server/src/test/kotlin/content/RandomEventManager.kt b/Server/src/test/kotlin/content/RandomEventManager.kt index 075e40134..513e3f9b5 100644 --- a/Server/src/test/kotlin/content/RandomEventManager.kt +++ b/Server/src/test/kotlin/content/RandomEventManager.kt @@ -1,8 +1,11 @@ package content import TestUtils +import core.game.node.item.Item import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test +import org.rs09.consts.Items +import rs09.game.content.ame.RandomEventNPC import rs09.game.world.GameWorld class RandomEventManager { @@ -33,4 +36,41 @@ class RandomEventManager { TestUtils.advanceTicks(rs09.game.content.ame.RandomEventManager.MAX_DELAY_TICKS + 5) Assertions.assertNotNull(p.getAttribute("re-npc", null)) } + + @Test fun teleportAndNotePunishmentShouldNotAffectAlreadyNotedItems() { + val p = TestUtils.getMockPlayer("Shitforbrains") + p.setAttribute("tutorial:complete", true) + rs09.game.content.ame.RandomEventManager().login(p) + + p.inventory.add(Item(Items.RAW_SHARK_384, 1000)) + rs09.game.content.ame.RandomEventManager.getInstance(p)?.fireEvent() + p.getAttribute("re-npc")!!.noteAndTeleport() + + Assertions.assertEquals(1000, p.inventory.getAmount(Items.RAW_SHARK_384)) + } + + @Test fun teleportAndNotePunishmentShouldNoteNotableUnnotedItems() { + val p = TestUtils.getMockPlayer("shitforbrains2") + p.setAttribute("tutorial:complete", true) + rs09.game.content.ame.RandomEventManager().login(p) + + p.inventory.add(Item(4151, 5)) + rs09.game.content.ame.RandomEventManager.getInstance(p)?.fireEvent() + p.getAttribute("re-npc")!!.noteAndTeleport() + + Assertions.assertEquals(5, p.inventory.getAmount(4152)) + Assertions.assertEquals(0, p.inventory.getAmount(4151)) + } + + @Test fun teleportAndNotePunishmentShouldNotAffectUnnotableItems() { + val p = TestUtils.getMockPlayer("shitforbrains3") + p.setAttribute("tutorial:complete", true) + rs09.game.content.ame.RandomEventManager().login(p) + + p.inventory.add(Item(Items.AIR_RUNE_556, 30)) + rs09.game.content.ame.RandomEventManager.getInstance(p)?.fireEvent() + p.getAttribute("re-npc")!!.noteAndTeleport() + + Assertions.assertEquals(30, p.inventory.getAmount(Items.AIR_RUNE_556)) + } } \ No newline at end of file