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