diff --git a/Server/build.gradle b/Server/build.gradle index 12c015ce9..62a7c9890 100644 --- a/Server/build.gradle +++ b/Server/build.gradle @@ -44,6 +44,8 @@ dependencies { test { useJUnitPlatform() + dependsOn cleanTest + testLogging.showStandardStreams = true } /*sourceSets { diff --git a/Server/src/main/kotlin/rs09/game/ge/GEDB.kt b/Server/src/main/kotlin/rs09/game/ge/GEDB.kt index c50b49e76..cc4135b9a 100644 --- a/Server/src/main/kotlin/rs09/game/ge/GEDB.kt +++ b/Server/src/main/kotlin/rs09/game/ge/GEDB.kt @@ -18,13 +18,16 @@ object GEDB { private var pathString = "" private var connection: Connection? = null - //This needs to be a separate method, so we can call it after the server config has been parsed - fun init() - { - pathString = File(ServerConstants.GRAND_EXCHANGE_DATA_PATH + "grandexchange.db").absolutePath + fun init() { + init(File(ServerConstants.GRAND_EXCHANGE_DATA_PATH + "grandexchange.db").absolutePath) + } + //This needs to be a separate method, so we can call it after the server config has been parsed + fun init(path: String) + { + pathString = path //Check if the grandexchange.db file already exists. If not, create it and create the tables. - if(!File(pathString).exists()) + if(!File(path).exists()) generateAndTransfer() } diff --git a/Server/src/main/kotlin/rs09/game/ge/GrandExchange.kt b/Server/src/main/kotlin/rs09/game/ge/GrandExchange.kt index aae056b75..88da9176c 100644 --- a/Server/src/main/kotlin/rs09/game/ge/GrandExchange.kt +++ b/Server/src/main/kotlin/rs09/game/ge/GrandExchange.kt @@ -1,24 +1,23 @@ package rs09.game.ge -import api.StartupListener -import api.getItemName -import api.itemDefinition -import api.sendMessage +import api.* import core.game.ge.GrandExchangeDatabase import core.game.ge.OfferState import core.game.node.entity.player.Player import core.game.node.entity.player.info.PlayerDetails import core.game.node.entity.player.link.audio.Audio import rs09.game.system.SystemLogger +import rs09.game.system.command.Privilege import rs09.game.system.config.ItemConfigParser import rs09.game.world.repository.Repository import java.lang.Integer.max +import java.sql.ResultSet /** * Handles the exchanging of offers, offer update thread, etc. * @author Ceikry */ -class GrandExchange : StartupListener { +class GrandExchange : StartupListener, Commands { /** * Fallback safety check to make sure we don't start the GE twice under any circumstance */ @@ -41,23 +40,42 @@ class GrandExchange : StartupListener { val t = Thread { Thread.currentThread().name = "GE Update Worker" while(true) { - val conn = GEDB.connect() - val stmt = conn.createStatement() - val buy_offer = stmt.executeQuery("SELECT * from player_offers where is_sale = 0") - val buyOffers = ArrayList() + with(GEDB.connect()) { + val conn = this - while(buy_offer.next()) - { - val offer = GrandExchangeOffer.fromQuery(buy_offer) - if(!offer.isActive) continue - buyOffers.add(offer) + val botStmt = conn.createStatement() + val botOffers = botStmt.executeQuery("SELECT * from bot_offers") + 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 buyOffers = ArrayList() + while(buyOffer.next()) buyOffers.add(GrandExchangeOffer.fromQuery(buyOffer)) + buyStmt.close() + + for(offer in buyOffers.sortedBy { it.offeredValue }.reversed()) { + if (bot.amountLeft <= 0) break + exchange(bot, offer) + } + } + botStmt.close() + + 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() + + while(activeOffer.next()) + { + val offer = GrandExchangeOffer.fromQuery(activeOffer) + if(!offer.isActive) continue + activeOffers.add(offer) + } + + for(offer in activeOffers) + processOffer(offer) + + stmt.close() } - - for(offer in buyOffers) - processOffer(offer) - - stmt.close() - Thread.sleep(15_000) //sleep for 15 seconds } }.start() @@ -65,48 +83,53 @@ class GrandExchange : StartupListener { isRunning = true } + override fun defineCommands() { + define("addbotoffer", Privilege.ADMIN) {player, strings -> + val id = strings[1].toInt() + val amount = strings[2].toInt() + addBotOffer(id, amount) + notify(player, "Added ${amount}x ${getItemName(id)} to the bot offers.") + } + } + companion object { fun processOffer(offer: GrandExchangeOffer) { - val conn = GEDB.connect() - if(offer.isActive) - { - val sellStmt = conn.createStatement() - val sell_offer = sellStmt.executeQuery("SELECT * from player_offers where item_id = ${offer.itemID} AND is_sale = 1 AND offer_state < 4 AND NOT offer_state = 2") - var bestOffer: GrandExchangeOffer? = null - - while(sell_offer.next()) - { - val otherOffer = GrandExchangeOffer.fromQuery(sell_offer) - if(!otherOffer.isActive) continue - if(otherOffer.offeredValue > offer.offeredValue) continue - if(bestOffer == null) bestOffer = otherOffer - else if(otherOffer.offeredValue < bestOffer.offeredValue) bestOffer = otherOffer + with (GEDB.connect()) { + val conn = this + if(offer.isActive) { + val stmt = conn.createStatement() + val olderOffers = stmt.executeQuery("SELECT * FROM player_offers WHERE item_id = ${offer.itemID} AND is_sale = ${!offer.sell} AND offer_state < 4 AND NOT offer_state = 2 AND time_stamp < ${offer.timeStamp}") + if(tryOffers(offer, olderOffers, true)) return + val newerOffers = stmt.executeQuery("SELECT * FROM player_offers WHERE item_id = ${offer.itemID} AND is_sale = ${!offer.sell} AND offer_state < 4 AND NOT offer_state = 2 AND time_stamp >= ${offer.timeStamp}") + if(tryOffers(offer, newerOffers, false)) return } + } + } - if(bestOffer != null) - { - val before = offer.amountLeft - exchange(offer,bestOffer) - if(offer.amountLeft != before) - SystemLogger.logGE("Purchased ${before - offer.amountLeft}x ${getItemName(offer.itemID)} @ B:${offer.offeredValue}/S:${bestOffer.offeredValue} gp each.") - } + private fun tryOffers(offer: GrandExchangeOffer, set: ResultSet, offerBiased: Boolean) : Boolean { + var bestOffer: GrandExchangeOffer? = null + while(set.next()) { + val otherOffer = GrandExchangeOffer.fromQuery(set) + if (otherOffer.amountLeft < 1 || offer.amountLeft < 1) continue + val buyOffer = if(otherOffer.sell) offer else otherOffer + val sellOffer = if(otherOffer.sell) otherOffer else offer + if (buyOffer.offeredValue < sellOffer.offeredValue) continue + bestOffer = if (bestOffer == null) otherOffer + else compareOffers(offer, otherOffer, bestOffer, offerBiased) + } + set.close() + if(bestOffer != null) exchange(offer, bestOffer) + return bestOffer != null + } - if(offer.amountLeft > 0) - { - val botStmt = conn.createStatement() - val bot_offer = botStmt.executeQuery("SELECT * from bot_offers where item_id = ${offer.itemID}") - if(bot_offer.next()) - { - val botOffer = GrandExchangeOffer.fromBotQuery(bot_offer) - val before = offer.amountLeft - exchange(offer, botOffer) - if(offer.amountLeft != before) - SystemLogger.logGE("Purchased FROM BOT ${offer.amountLeft - before}x ${getItemName(offer.itemID)}") - } - botStmt.close() - } - sellStmt.close() + private fun compareOffers(offer: GrandExchangeOffer, first: GrandExchangeOffer, second: GrandExchangeOffer, biased: Boolean) : GrandExchangeOffer { + return if(offer.sell) { + if(biased) if(first.offeredValue > second.offeredValue) first else second + else if(first.offeredValue < second.offeredValue) first else second + } else { + if(biased) if(first.offeredValue < second.offeredValue) first else second + else if(first.offeredValue > second.offeredValue) first else second } } @@ -233,6 +256,8 @@ class GrandExchange : StartupListener { val seller = if(offer.sell) offer else other val buyer = if(offer == seller) other else offer + val sellerBias = seller.timeStamp > buyer.timeStamp + //If the buyer is buying for less than the seller is selling for, don't exchange if(seller.offeredValue > buyer.offeredValue) return @@ -242,10 +267,10 @@ class GrandExchange : StartupListener { if(seller.amountLeft < 1 && seller.player != null) seller.player!!.audioManager.send(Audio(4042,1,1)) - seller.addWithdrawItem(995, amount * seller.offeredValue) + seller.addWithdrawItem(995, amount * if(sellerBias) buyer.offeredValue else seller.offeredValue) buyer.addWithdrawItem(seller.itemID, amount) - if(seller.offeredValue < buyer.offeredValue) + if(!sellerBias) buyer.addWithdrawItem(995, amount * (buyer.offeredValue - seller.offeredValue)) if(seller.amountLeft < 1) @@ -253,8 +278,8 @@ class GrandExchange : StartupListener { if(buyer.amountLeft < 1) buyer.offerState = OfferState.COMPLETED - seller.totalCoinExchange += seller.offeredValue * amount - buyer.totalCoinExchange += seller.offeredValue * amount + seller.totalCoinExchange += if(sellerBias) buyer.offeredValue else seller.offeredValue * amount + buyer.totalCoinExchange += if(sellerBias) buyer.offeredValue else seller.offeredValue * amount seller.update() val sellerPlayer = Repository.uid_map[seller.playerUID] diff --git a/Server/src/main/kotlin/rs09/game/ge/GrandExchangeOffer.kt b/Server/src/main/kotlin/rs09/game/ge/GrandExchangeOffer.kt index 5e20a77d0..03604c49b 100644 --- a/Server/src/main/kotlin/rs09/game/ge/GrandExchangeOffer.kt +++ b/Server/src/main/kotlin/rs09/game/ge/GrandExchangeOffer.kt @@ -224,6 +224,7 @@ class GrandExchangeOffer() { o.itemID = result.getInt("item_id") o.offeredValue = GrandExchange.getRecommendedPrice(o.itemID, true) o.isBot = true + o.timeStamp = System.currentTimeMillis() return o } diff --git a/Server/src/test/kotlin/ExchangeTests.kt b/Server/src/test/kotlin/ExchangeTests.kt new file mode 100644 index 000000000..c7711498c --- /dev/null +++ b/Server/src/test/kotlin/ExchangeTests.kt @@ -0,0 +1,69 @@ +import core.game.ge.OfferState +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.fail +import rs09.game.ge.GEDB +import rs09.game.ge.GrandExchange +import rs09.game.ge.GrandExchangeOffer +import rs09.game.system.SystemLogger +import kotlin.random.Random + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class ExchangeTests { + companion object { + private const val TEST_DB_PATH = "ge_test.db" + init { + GEDB.init(TEST_DB_PATH) + } + + fun generateOffer(itemId: Int, amount: Int, price: Int, sale: Boolean) : GrandExchangeOffer { + val offer = GrandExchangeOffer() + val uid = "test ${System.currentTimeMillis()}".hashCode() + offer.offerState = OfferState.REGISTERED + offer.itemID = itemId + offer.offeredValue = price + offer.amount = amount + offer.timeStamp = System.currentTimeMillis() + offer.index = 0 + offer.isBot = false + offer.playerUID = uid + offer.sell = sale + offer.writeNew() + return offer + } + } + + @Test fun testPlaceOffer() { + val offer = generateOffer(4151, 1, 100000, true) + val uid = offer.playerUID + + with (GEDB.connect()) { + val stmt = this.createStatement() + val result = stmt.executeQuery("select * from player_offers where player_uid = $uid") + val thisOffer = if(result.next()) GrandExchangeOffer.fromQuery(result) else fail("Offer did not exist!") + Assertions.assertEquals(offer.itemID, thisOffer.itemID) + } + } + + @Test fun testBuyFirstSellAfterFavorsSell() { + val buyOffer = generateOffer(4151, 1, 100000, false) + val sellOffer = generateOffer(4151, 1, 85000, true) + GrandExchange.exchange(buyOffer, sellOffer) + Assertions.assertEquals(null, buyOffer.withdraw[1], "Buyer got coins back on sell bias") //should get no coins back + } + + @Test fun testSellFirstBuyAfterFavorsBuy() { + val sellOffer = generateOffer(4151, 1, 85000, true) + val buyOffer = generateOffer(4151, 1, 100000, false) + GrandExchange.exchange(sellOffer,buyOffer) + Assertions.assertNotEquals(null, buyOffer.withdraw[1], "Buyer did not get coins back on buy bias") //should get coins back + } + + @Test fun buyerCannotBuyHigherSellOffer() { + val sellOffer = generateOffer(4151, 1, 125000, true) + val buyOffer = generateOffer(4151, 1, 100000, false) + GrandExchange.exchange(sellOffer,buyOffer) + Assertions.assertEquals(null, buyOffer.withdraw[0]) //Buyer should not get any items, sell offer is higher + } +} \ No newline at end of file