Added unit tests for the GE
GE should now favor the buy offer if it's newer than the paired sell offer (buyer gets GP back) GE should now favor the sell offer if it's newer than the paired buy offer (buyer does NOT get gp back) GE should now always award bot offers to the highest bidder
This commit is contained in:
@@ -44,6 +44,8 @@ dependencies {
|
|||||||
|
|
||||||
test {
|
test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
|
dependsOn cleanTest
|
||||||
|
testLogging.showStandardStreams = true
|
||||||
}
|
}
|
||||||
|
|
||||||
/*sourceSets {
|
/*sourceSets {
|
||||||
|
|||||||
@@ -18,13 +18,16 @@ object GEDB {
|
|||||||
private var pathString = ""
|
private var pathString = ""
|
||||||
private var connection: Connection? = null
|
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() {
|
||||||
fun init()
|
init(File(ServerConstants.GRAND_EXCHANGE_DATA_PATH + "grandexchange.db").absolutePath)
|
||||||
{
|
}
|
||||||
pathString = 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.
|
//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()
|
generateAndTransfer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,23 @@
|
|||||||
package rs09.game.ge
|
package rs09.game.ge
|
||||||
|
|
||||||
import api.StartupListener
|
import api.*
|
||||||
import api.getItemName
|
|
||||||
import api.itemDefinition
|
|
||||||
import api.sendMessage
|
|
||||||
import core.game.ge.GrandExchangeDatabase
|
import core.game.ge.GrandExchangeDatabase
|
||||||
import core.game.ge.OfferState
|
import core.game.ge.OfferState
|
||||||
import core.game.node.entity.player.Player
|
import core.game.node.entity.player.Player
|
||||||
import core.game.node.entity.player.info.PlayerDetails
|
import core.game.node.entity.player.info.PlayerDetails
|
||||||
import core.game.node.entity.player.link.audio.Audio
|
import core.game.node.entity.player.link.audio.Audio
|
||||||
import rs09.game.system.SystemLogger
|
import rs09.game.system.SystemLogger
|
||||||
|
import rs09.game.system.command.Privilege
|
||||||
import rs09.game.system.config.ItemConfigParser
|
import rs09.game.system.config.ItemConfigParser
|
||||||
import rs09.game.world.repository.Repository
|
import rs09.game.world.repository.Repository
|
||||||
import java.lang.Integer.max
|
import java.lang.Integer.max
|
||||||
|
import java.sql.ResultSet
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the exchanging of offers, offer update thread, etc.
|
* Handles the exchanging of offers, offer update thread, etc.
|
||||||
* @author Ceikry
|
* @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
|
* 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 {
|
val t = Thread {
|
||||||
Thread.currentThread().name = "GE Update Worker"
|
Thread.currentThread().name = "GE Update Worker"
|
||||||
while(true) {
|
while(true) {
|
||||||
val conn = GEDB.connect()
|
with(GEDB.connect()) {
|
||||||
val stmt = conn.createStatement()
|
val conn = this
|
||||||
val buy_offer = stmt.executeQuery("SELECT * from player_offers where is_sale = 0")
|
|
||||||
val buyOffers = ArrayList<GrandExchangeOffer>()
|
|
||||||
|
|
||||||
while(buy_offer.next())
|
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<GrandExchangeOffer>()
|
||||||
|
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<GrandExchangeOffer>()
|
||||||
|
|
||||||
|
while(activeOffer.next())
|
||||||
{
|
{
|
||||||
val offer = GrandExchangeOffer.fromQuery(buy_offer)
|
val offer = GrandExchangeOffer.fromQuery(activeOffer)
|
||||||
if(!offer.isActive) continue
|
if(!offer.isActive) continue
|
||||||
buyOffers.add(offer)
|
activeOffers.add(offer)
|
||||||
}
|
}
|
||||||
|
|
||||||
for(offer in buyOffers)
|
for(offer in activeOffers)
|
||||||
processOffer(offer)
|
processOffer(offer)
|
||||||
|
|
||||||
stmt.close()
|
stmt.close()
|
||||||
|
}
|
||||||
Thread.sleep(15_000) //sleep for 15 seconds
|
Thread.sleep(15_000) //sleep for 15 seconds
|
||||||
}
|
}
|
||||||
}.start()
|
}.start()
|
||||||
@@ -65,48 +83,53 @@ class GrandExchange : StartupListener {
|
|||||||
isRunning = true
|
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 {
|
companion object {
|
||||||
fun processOffer(offer: GrandExchangeOffer)
|
fun processOffer(offer: GrandExchangeOffer)
|
||||||
{
|
{
|
||||||
val conn = GEDB.connect()
|
with (GEDB.connect()) {
|
||||||
if(offer.isActive)
|
val conn = this
|
||||||
{
|
if(offer.isActive) {
|
||||||
val sellStmt = conn.createStatement()
|
val stmt = 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")
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tryOffers(offer: GrandExchangeOffer, set: ResultSet, offerBiased: Boolean) : Boolean {
|
||||||
var bestOffer: GrandExchangeOffer? = null
|
var bestOffer: GrandExchangeOffer? = null
|
||||||
|
while(set.next()) {
|
||||||
while(sell_offer.next())
|
val otherOffer = GrandExchangeOffer.fromQuery(set)
|
||||||
{
|
if (otherOffer.amountLeft < 1 || offer.amountLeft < 1) continue
|
||||||
val otherOffer = GrandExchangeOffer.fromQuery(sell_offer)
|
val buyOffer = if(otherOffer.sell) offer else otherOffer
|
||||||
if(!otherOffer.isActive) continue
|
val sellOffer = if(otherOffer.sell) otherOffer else offer
|
||||||
if(otherOffer.offeredValue > offer.offeredValue) continue
|
if (buyOffer.offeredValue < sellOffer.offeredValue) continue
|
||||||
if(bestOffer == null) bestOffer = otherOffer
|
bestOffer = if (bestOffer == null) otherOffer
|
||||||
else if(otherOffer.offeredValue < bestOffer.offeredValue) bestOffer = otherOffer
|
else compareOffers(offer, otherOffer, bestOffer, offerBiased)
|
||||||
|
}
|
||||||
|
set.close()
|
||||||
|
if(bestOffer != null) exchange(offer, bestOffer)
|
||||||
|
return bestOffer != null
|
||||||
}
|
}
|
||||||
|
|
||||||
if(bestOffer != null)
|
private fun compareOffers(offer: GrandExchangeOffer, first: GrandExchangeOffer, second: GrandExchangeOffer, biased: Boolean) : GrandExchangeOffer {
|
||||||
{
|
return if(offer.sell) {
|
||||||
val before = offer.amountLeft
|
if(biased) if(first.offeredValue > second.offeredValue) first else second
|
||||||
exchange(offer,bestOffer)
|
else if(first.offeredValue < second.offeredValue) first else second
|
||||||
if(offer.amountLeft != before)
|
} else {
|
||||||
SystemLogger.logGE("Purchased ${before - offer.amountLeft}x ${getItemName(offer.itemID)} @ B:${offer.offeredValue}/S:${bestOffer.offeredValue} gp each.")
|
if(biased) if(first.offeredValue < second.offeredValue) first else second
|
||||||
}
|
else if(first.offeredValue > second.offeredValue) first else second
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,6 +256,8 @@ class GrandExchange : StartupListener {
|
|||||||
val seller = if(offer.sell) offer else other
|
val seller = if(offer.sell) offer else other
|
||||||
val buyer = if(offer == seller) other else offer
|
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 the buyer is buying for less than the seller is selling for, don't exchange
|
||||||
if(seller.offeredValue > buyer.offeredValue) return
|
if(seller.offeredValue > buyer.offeredValue) return
|
||||||
|
|
||||||
@@ -242,10 +267,10 @@ class GrandExchange : StartupListener {
|
|||||||
if(seller.amountLeft < 1 && seller.player != null)
|
if(seller.amountLeft < 1 && seller.player != null)
|
||||||
seller.player!!.audioManager.send(Audio(4042,1,1))
|
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)
|
buyer.addWithdrawItem(seller.itemID, amount)
|
||||||
|
|
||||||
if(seller.offeredValue < buyer.offeredValue)
|
if(!sellerBias)
|
||||||
buyer.addWithdrawItem(995, amount * (buyer.offeredValue - seller.offeredValue))
|
buyer.addWithdrawItem(995, amount * (buyer.offeredValue - seller.offeredValue))
|
||||||
|
|
||||||
if(seller.amountLeft < 1)
|
if(seller.amountLeft < 1)
|
||||||
@@ -253,8 +278,8 @@ class GrandExchange : StartupListener {
|
|||||||
if(buyer.amountLeft < 1)
|
if(buyer.amountLeft < 1)
|
||||||
buyer.offerState = OfferState.COMPLETED
|
buyer.offerState = OfferState.COMPLETED
|
||||||
|
|
||||||
seller.totalCoinExchange += seller.offeredValue * amount
|
seller.totalCoinExchange += if(sellerBias) buyer.offeredValue else seller.offeredValue * amount
|
||||||
buyer.totalCoinExchange += seller.offeredValue * amount
|
buyer.totalCoinExchange += if(sellerBias) buyer.offeredValue else seller.offeredValue * amount
|
||||||
|
|
||||||
seller.update()
|
seller.update()
|
||||||
val sellerPlayer = Repository.uid_map[seller.playerUID]
|
val sellerPlayer = Repository.uid_map[seller.playerUID]
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ class GrandExchangeOffer() {
|
|||||||
o.itemID = result.getInt("item_id")
|
o.itemID = result.getInt("item_id")
|
||||||
o.offeredValue = GrandExchange.getRecommendedPrice(o.itemID, true)
|
o.offeredValue = GrandExchange.getRecommendedPrice(o.itemID, true)
|
||||||
o.isBot = true
|
o.isBot = true
|
||||||
|
o.timeStamp = System.currentTimeMillis()
|
||||||
return o
|
return o
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user