From f36faa0f093aa13a96b12d0564b9b2f98fc47acc Mon Sep 17 00:00:00 2001 From: ceikry Date: Thu, 10 Mar 2022 20:55:44 -0600 Subject: [PATCH] Majority of GE work --- Server/build.gradle | 3 +- Server/src/main/kotlin/rs09/Server.kt | 2 + Server/src/main/kotlin/rs09/game/ge/GEDB.kt | 155 ++++++++++++++++ .../main/kotlin/rs09/game/ge/GrandExchange.kt | 115 ++++++++++-- .../kotlin/rs09/game/ge/GrandExchangeOffer.kt | 169 +++++++++++++++++- .../main/kotlin/rs09/game/ge/OfferManager.kt | 3 - .../rs09/game/ge/PlayerGrandExchange.kt | 8 +- .../entity/player/info/login/LoginParser.kt | 8 +- .../world/repository/DisconnectionQueue.kt | 2 +- .../rs09/game/world/repository/Repository.kt | 14 ++ 10 files changed, 450 insertions(+), 29 deletions(-) create mode 100644 Server/src/main/kotlin/rs09/game/ge/GEDB.kt diff --git a/Server/build.gradle b/Server/build.gradle index 547596519..e4bda9b43 100644 --- a/Server/build.gradle +++ b/Server/build.gradle @@ -32,7 +32,8 @@ dependencies { "libs/classgraph-4.8.98.jar", "libs/mysql-connector-java-8.0.21.jar", "libs/mordant-jvm-2.0.0-alpha2.jar", - "libs/colormath-jvm-2.0.0.jar" + "libs/colormath-jvm-2.0.0.jar", + "libs/sqlite-jdbc.jar" ) } diff --git a/Server/src/main/kotlin/rs09/Server.kt b/Server/src/main/kotlin/rs09/Server.kt index d030a0ee8..25c76a28e 100644 --- a/Server/src/main/kotlin/rs09/Server.kt +++ b/Server/src/main/kotlin/rs09/Server.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import rs09.game.content.global.GlobalKillCounter; import rs09.game.ge.GEAutoStock +import rs09.game.ge.GEDB import rs09.game.system.SystemLogger import rs09.game.system.config.ServerConfigParser import rs09.game.world.GameWorld @@ -64,6 +65,7 @@ object Server { SystemLogger.logInfo("Using config file: ${"worldprops" + File.separator + "default.conf"}") ServerConfigParser.parse("worldprops" + File.separator + "default.conf") } + GEDB.init() startTime = System.currentTimeMillis() val t = TimeStamp() SystemLogger.logInfo("Initializing Server Store...") diff --git a/Server/src/main/kotlin/rs09/game/ge/GEDB.kt b/Server/src/main/kotlin/rs09/game/ge/GEDB.kt new file mode 100644 index 000000000..d3d108c38 --- /dev/null +++ b/Server/src/main/kotlin/rs09/game/ge/GEDB.kt @@ -0,0 +1,155 @@ +package rs09.game.ge + +import core.cache.def.impl.ItemDefinition +import core.game.ge.OfferState +import core.game.node.entity.player.link.audio.Audio +import org.json.simple.JSONArray +import org.json.simple.JSONObject +import org.json.simple.parser.JSONParser +import rs09.ServerConstants +import java.io.File +import java.io.FileReader +import java.lang.Integer.min +import java.sql.Connection +import java.sql.DriverManager + +/** + * Collection of methods for interacting with the grand exchange databases + * @author Ceikry + */ +object GEDB { + var pathString = "" + + //This needs to be a separate method, so we can call it after the server config has been parsed + fun init() + { + pathString = ServerConstants.GRAND_EXCHANGE_DATA_PATH + File.separator + "grandexchange.db" + + //Check if the grandexchange.db file already exists. If not, create it and create the tables. + if(!File(pathString).exists()) + generateAndTransfer() + } + + fun connect(): Connection + { + return DriverManager.getConnection("jdbc:sqlite:$pathString") + } + + private fun convertJsonArray(arr: JSONArray): String + { + val sb = StringBuilder() + for ((index, data) in arr.withIndex()) + { + val item = data as JSONObject + sb.append("$index") + sb.append(",") + sb.append(item["id"]) + sb.append(",") + sb.append(item["amount"]) + if(index + 1 < arr.size) + sb.append(":") + } + return sb.toString() + } + + private fun generateAndTransfer() + { + val conn = connect() //for sqlite jdbc, attempting to connect to a non-existing .db file creates it. + val statement = conn.createStatement() + + //table for tracking player offers - replaces offer_dispatch.json + statement.execute( + "CREATE TABLE player_offers(" + + "uid INTEGER PRIMARY KEY ASC," + + "player_uid INTEGER," + + "item_id INTEGER," + + "amount_total INTEGER," + + "amount_complete INTEGER," + + "offered_value INTEGER," + + "time_stamp INTEGER," + + "offer_state INTEGER," + + "is_sale INTEGER," + + "withdraw_items STRING ," + + "total_coin_xc INTEGER)" + ) + + //table for tracking bot offers - replaces bot_offers.json + statement.execute( + "CREATE TABLE bot_offers(" + + "item_id INTEGER," + + "amount INTEGER)" + ) + + //table for tracking price index data - replaces gedb.xml + statement.execute( + "CREATE TABLE price_index(" + + "item_id INTEGER," + + "value INTEGER," + + "total_value INTEGER," + + "unique_trades INTEGER," + + "last_update INTEGER)" + ) + + //check if the old .json and .xml files exist, and if they do, read them into the sqlite database and remove them. + val playerJson = ServerConstants.GRAND_EXCHANGE_DATA_PATH + "offer_dispatch.json" + val botJson = ServerConstants.GRAND_EXCHANGE_DATA_PATH + "bot_offers.json" + + if(File(playerJson).exists()) + { + val parser = JSONParser() + val reader = FileReader(playerJson) + val saveFile = parser.parse(reader) as JSONObject + + if (saveFile.containsKey("offers")) { + val offers = saveFile["offers"] as JSONArray + + for (offer in offers) { + val o = offer as JSONObject + statement.execute("insert into player_offers(player_uid,item_id,amount_total,amount_complete,offered_value,time_stamp,offer_state,is_sale,withdraw_items,total_coin_xc) " + + "values(" + + "${o["playerUID"]}," + + "${o["itemId"]}," + + "${o["amount"]}," + + "${o["completedAmount"]}," + + "${o["offeredValue"]}," + + "${o["timeStamp"]}," + + "${o["offerState"]}," + + "${if (o["sale"] as Boolean) 1 else 0}," + + "'" + convertJsonArray(o["withdrawItems"] as JSONArray) + "'," + + "${o["totalCoinExchange"]})" + ) + + } + } + + reader.close() + //File(playerJson).delete() + } + + if(File(botJson).exists()) + { + val parser = JSONParser() + val reader = FileReader(botJson) + val saveFile = parser.parse(reader) as JSONObject + + if (saveFile.containsKey("offers")) { + val offers = saveFile["offers"] as JSONArray + + for (offer in offers) { + val o = offer as JSONObject + statement.execute("insert into bot_offers " + + "values(${o["item"]},${o["qty"]})") + } + } + reader.close() + } + + + //price index isn't worth transferring, so we're just going to make a new one. + ItemDefinition.getDefinitions().values.forEach { def -> + if(def.isTradeable){ + statement.execute("insert into price_index(item_id, value) values(${def.id},${def.value})") + } + } + } +} \ No newline at end of file diff --git a/Server/src/main/kotlin/rs09/game/ge/GrandExchange.kt b/Server/src/main/kotlin/rs09/game/ge/GrandExchange.kt index b5e3a3ae5..edf31c6dc 100644 --- a/Server/src/main/kotlin/rs09/game/ge/GrandExchange.kt +++ b/Server/src/main/kotlin/rs09/game/ge/GrandExchange.kt @@ -1,7 +1,15 @@ package rs09.game.ge +import api.getItemName +import api.sendMessage +import core.cache.def.impl.ItemDefinition +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 core.game.world.callback.CallBack import rs09.game.system.SystemLogger +import rs09.game.world.repository.Repository import rs09.tools.secondsToTicks object GrandExchange : CallBack { @@ -14,15 +22,12 @@ object GrandExchange : CallBack { * Initializes the offer manager and spawns an update thread. * @param local whether or not the GE should be the local in-code server rather than some hypothetical remote implementation. */ - fun boot(local: Boolean){ + fun boot(){ if(isRunning) return - if(!local){ - TODO("Remote GE server stuff") - } SystemLogger.logGE("Initializing GE...") - OfferManager.init() + //OfferManager.init() SystemLogger.logGE("GE Initialized.") SystemLogger.logGE("Initializing GE Update Worker") @@ -31,11 +36,39 @@ object GrandExchange : CallBack { Thread.currentThread().name = "GE Update Worker" while(true) { SystemLogger.logGE("Updating offers...") - OfferManager.update() - if(OfferManager.dumpDatabase){ - SystemLogger.logGE("Saving GE...") - OfferManager.save() - OfferManager.dumpDatabase = false + val conn = GEDB.connect() + val stmt = conn.createStatement() + val buy_offer = stmt.executeQuery("SELECT * from player_offers where is_sale = 0") + + while(buy_offer.next()) + { + val offer = GrandExchangeOffer.fromQuery(buy_offer) + if(offer.isActive) + { + val sell_offer = stmt.executeQuery("SELECT * from player_offers where is_sale = 1 AND item_id = ${offer.itemID}") + while(sell_offer.next()) + { + val otherOffer = GrandExchangeOffer.fromQuery(sell_offer) + if(!otherOffer.isActive) continue + val before = offer.amountLeft + exchange(offer,otherOffer) + if(offer.amountLeft != before) + SystemLogger.logGE("Purchased ${offer.amountLeft - before}x ${getItemName(offer.itemID)} @ ${offer.offeredValue}/${otherOffer.offeredValue} gp each.") + } + + if(offer.amountLeft > 0) + { + val bot_offer = stmt.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)}") + } + } + } } Thread.sleep(60_000) //sleep for 60 seconds } @@ -44,8 +77,68 @@ object GrandExchange : CallBack { isRunning = true } + fun dispatch(player: Player, offer: GrandExchangeOffer) : Boolean + { + if ( offer.amount < 1 ) + sendMessage(player, "You must choose the quantity you wish to buy!").also { return false } + + if ( offer.offeredValue < 1 ) + sendMessage(player, "You must choose the price you wish to buy for!").also { return false } + + if ( offer.offerState != OfferState.PENDING || offer.uid != 0L ) + return false + + if ( player.isArtificial ) + offer.playerUID = PlayerDetails.getDetails("2009scape").uid.also { offer.isBot = true } + else + offer.playerUID = player.details.uid + + offer.offerState = OfferState.REGISTERED + player.playerGrandExchange.update(offer) + + if (offer.sell) { + Repository.sendNews(player.username + " just offered " + offer.amount + " " + getItemName(offer.itemID) + " on the GE.") + } + + offer.writeNew() + return true + } + + fun exchange(offer: GrandExchangeOffer, other: GrandExchangeOffer) + { + if(offer.sell && other.sell) return //Don't exchange if they are both sell offers + val amount = Integer.min(offer.amount - offer.completedAmount, other.amount - other.completedAmount) + + val seller = if(offer.sell) offer else other + val buyer = if(offer == seller) other else offer + + //If the buyer is buying for less than the seller is selling for, don't exchange + if(seller.offeredValue > buyer.offeredValue) return + + seller.completedAmount += amount + buyer.completedAmount += amount + + if(seller.amountLeft < 1 && seller.player != null) + seller.player!!.audioManager.send(Audio(4042,1,1)) + + seller.addWithdrawItem(995, amount * buyer.offeredValue) + buyer.addWithdrawItem(seller.itemID, amount) + + if(seller.offeredValue < buyer.offeredValue) + buyer.addWithdrawItem(995, amount * (buyer.offeredValue - seller.offeredValue)) + + if(seller.amountLeft < 1) + seller.offerState = OfferState.COMPLETED + if(buyer.amountLeft < 1) + buyer.offerState = OfferState.COMPLETED + + seller.update() + buyer.update() + } + override fun call(): Boolean { - boot(true) + GEDB.init() + boot() return true } } \ No newline at end of file diff --git a/Server/src/main/kotlin/rs09/game/ge/GrandExchangeOffer.kt b/Server/src/main/kotlin/rs09/game/ge/GrandExchangeOffer.kt index 6e1038e38..9619a0fde 100644 --- a/Server/src/main/kotlin/rs09/game/ge/GrandExchangeOffer.kt +++ b/Server/src/main/kotlin/rs09/game/ge/GrandExchangeOffer.kt @@ -4,12 +4,19 @@ import core.cache.def.impl.ItemDefinition import core.game.ge.OfferState import core.game.node.entity.player.Player import core.game.node.item.Item +import core.net.packet.PacketRepository +import core.net.packet.context.ContainerContext +import core.net.packet.context.GrandExchangeContext +import core.net.packet.out.ContainerPacket +import core.net.packet.out.GrandExchangePacket +import rs09.game.world.repository.Repository +import java.sql.ResultSet /** - * A struct holding all the data for grand exchange offers as stored in json database. + * A struct holding all the data for grand exchange offers. * - * @author Angle + * @author Ceikry */ class GrandExchangeOffer() { @@ -27,6 +34,7 @@ class GrandExchangeOffer() { var player: Player? = null var playerUID = 0 var isLimitation = false + var isBot = false /** * Gets the total amount of money entered. @@ -49,7 +57,164 @@ class GrandExchangeOffer() { val isActive: Boolean get() = offerState != OfferState.ABORTED && offerState != OfferState.PENDING && offerState != OfferState.COMPLETED && offerState != OfferState.REMOVED + fun addWithdrawItem(id: Int, amount: Int) + { + //loop checking if the item is already present first + for(item in withdraw) + if(item != null && item.id == id) + { + item.amount += amount + return + } + + //if we make it to this point, the item was not present. Loop to find first null slot and stick item there. + for((index,item) in withdraw.withIndex()) + if(item == null) + { + withdraw[index] = Item(id, amount) + return + } + + //send container update packet to player if they exist (are online) + if ( player != null ) + PacketRepository.send(ContainerPacket::class.java, ContainerContext(player, -1, -1757, 523 + index, withdraw, false)) + } + + fun visualize(player: Player) + { + PacketRepository.send( + GrandExchangePacket::class.java, + GrandExchangeContext(player, index.toByte(), offerState.ordinal.toByte(), itemID.toShort(), + sell, offeredValue, amount, completedAmount, totalCoinExchange) + ) + } + + fun update() + { + val conn = GEDB.connect() + + if(isBot) + { + val stmt = conn.prepareStatement("UPDATE bot_offers SET amount = ? WHERE item_id = ?") + stmt.setInt(0, amountLeft) + stmt.setInt(1, itemID) + stmt.executeUpdate() + } + else + { + val stmt = conn.prepareStatement("UPDATE player_offers SET amount_left = ?, offer_state = ?, total_coin_xc = ?, withdraw_items = ? WHERE uid = ?") + stmt.setInt(0, amountLeft) + stmt.setInt(1, offerState.ordinal) + stmt.setInt(2, totalCoinExchange) + stmt.setString(3, encodeWithdraw()) + stmt.setLong(4, uid) + stmt.executeUpdate() + } + } + + /** Called when writing a brand new offer to the database. Should not be used under any other circumstance **/ + fun writeNew() + { + val conn = GEDB.connect() + + if(isBot) + { + val stmt = conn.createStatement() + val result = stmt.executeQuery("SELECT * from bot_offers where item_id = $itemID") + val isExists = result.next() + + if(isExists) + { + val oldAmount = result.getInt("amount") + stmt.executeUpdate("UPDATE bot_offers set amount = ${oldAmount + amount} where item_id = $itemID") + } + else + stmt.executeUpdate("INSERT INTO bot_offers(item_id,amount) values($itemID,$amount)") + } + else + { + val stmt = conn.prepareStatement("INSERT INTO player_offers(player_uid, item_id, amount_total, offered_value, time_stamp, offer_state, is_sale) values(?,?,?,?,?,?,?,?)") + stmt.setInt(0, playerUID) + stmt.setInt(1, itemID) + stmt.setInt(2, amount) + stmt.setInt(3, offeredValue) + stmt.setLong(4, System.currentTimeMillis()) + stmt.setInt(5, offerState.ordinal) + stmt.setInt(6, if(sell) 1 else 0) + stmt.executeUpdate() + } + } + + private fun encodeWithdraw() : String + { + val sb = StringBuilder() + for((index, item) in withdraw.withIndex()) + { + sb.append(index) + sb.append(",") + if(item == null) + sb.append("null") + else + sb.append(item.id) + sb.append(",") + if(item == null) + sb.append("null") + else + sb.append(item.amount) + + if(index + 1 < withdraw.size) + sb.append(":") + } + + return sb.toString() + } + override fun toString(): String { return "[name=" + ItemDefinition.forId(itemID).name + ", itemId=" + itemID + ", amount=" + amount + ", completedAmount=" + completedAmount + ", offeredValue=" + offeredValue + ", index=" + index + ", sell=" + sell + ", state=" + offerState + ", withdraw=" + withdraw.contentToString() + ", totalCoinExchange=" + totalCoinExchange + ", playerUID=" + playerUID + "]" } + + companion object { + fun fromQuery(result: ResultSet): GrandExchangeOffer + { + val o = GrandExchangeOffer() + o.itemID = result.getInt("item_id") + o.amount = result.getInt("amount_total") + o.completedAmount = result.getInt("amount_completed") + o.offeredValue = result.getInt("offered_value") + o.sell = result.getInt("is_sale") == 1 + o.offerState = OfferState.values()[result.getInt("offer_state")] + o.uid = result.getLong("uid") + o.timeStamp = result.getLong("time_stamp") + + val itemString = result.getString("withdraw_items") + val items = itemString.split(":") + for(item in items) + { + val tokens = item.split(",") + val index = tokens[0].toInt() + if(tokens[1] == "null") continue //Skip null slots + o.withdraw[index] = Item(tokens[1].toInt(), tokens[2].toInt()) + } + + o.totalCoinExchange = result.getInt("total_coin_xc") + o.playerUID = result.getInt("player_uid") + + if(Repository.uid_map[o.playerUID] != null) + o.player = Repository.uid_map[o.playerUID] + + return o + } + + fun fromBotQuery(result: ResultSet): GrandExchangeOffer + { + val o = GrandExchangeOffer() + o.sell = result.getInt("is_sale") == 1 + o.amount = result.getInt("amount_total") + o.offerState = OfferState.REGISTERED + o.itemID = result.getInt("item_id") + o.offeredValue = OfferManager.getRecommendedPrice(o.itemID, true) + o.isBot = true + return o + } + } } \ No newline at end of file diff --git a/Server/src/main/kotlin/rs09/game/ge/OfferManager.kt b/Server/src/main/kotlin/rs09/game/ge/OfferManager.kt index fc722a6be..abb82839c 100644 --- a/Server/src/main/kotlin/rs09/game/ge/OfferManager.kt +++ b/Server/src/main/kotlin/rs09/game/ge/OfferManager.kt @@ -342,16 +342,13 @@ object OfferManager { fun dispatch(player: Player, offer: GrandExchangeOffer): Boolean { if (offer.amount < 1) { player.packetDispatch.sendMessage("You must choose the quantity you wish to buy!") - println("amountthing") return false } if (offer.offeredValue < 1) { player.packetDispatch.sendMessage("You must choose the price you wish to buy for!") - println("pricethng") return false } if (offer.offerState != OfferState.PENDING || offer.uid != 0L) { - println("pendingthing") return false } if (player.isArtificial) { diff --git a/Server/src/main/kotlin/rs09/game/ge/PlayerGrandExchange.kt b/Server/src/main/kotlin/rs09/game/ge/PlayerGrandExchange.kt index 25f262023..1267a5388 100644 --- a/Server/src/main/kotlin/rs09/game/ge/PlayerGrandExchange.kt +++ b/Server/src/main/kotlin/rs09/game/ge/PlayerGrandExchange.kt @@ -296,13 +296,7 @@ class PlayerGrandExchange(private val player: Player) { * @param offer The offer to update. */ fun update(offer: GrandExchangeOffer?) { - if (offer != null) { - PacketRepository.send( - GrandExchangePacket::class.java, - GrandExchangeContext(player, offer.index.toByte(), offer.offerState.ordinal.toByte(), offer.itemID.toShort(), - offer.sell, offer.offeredValue, offer.amount, offer.completedAmount, offer.totalCoinExchange) - ) - } + offer?.visualize(player) } /** 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 1bd578c62..881d369bd 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 @@ -106,7 +106,7 @@ class LoginParser( return } if(!PlayerParser.parse(player)){ - Repository.players.remove(player) + Repository.removePlayer(player) Repository.LOGGED_IN_PLAYERS.remove(player.username) Repository.lobbyPlayers.remove(player) Repository.playerNames.remove(player.name) @@ -124,10 +124,10 @@ class LoginParser( p.clear() Repository.playerNames.remove(p.name) Repository.lobbyPlayers.remove(p) - Repository.players.remove(p) + Repository.removePlayer(p) } if (!Repository.players.contains(player)) { - Repository.players.add(player) + Repository.addPlayer(player) } player.details.session.setObject(player) flag(Response.SUCCESSFUL) @@ -179,7 +179,7 @@ class LoginParser( GameWorld.Pulser.submit(object : Pulse(1) { override fun pulse(): Boolean { if (!Repository.players.contains(player)) { - Repository.players.add(player) + Repository.addPlayer(player) } return true } diff --git a/Server/src/main/kotlin/rs09/game/world/repository/DisconnectionQueue.kt b/Server/src/main/kotlin/rs09/game/world/repository/DisconnectionQueue.kt index 5ee8ecab6..5ba79b7b2 100644 --- a/Server/src/main/kotlin/rs09/game/world/repository/DisconnectionQueue.kt +++ b/Server/src/main/kotlin/rs09/game/world/repository/DisconnectionQueue.kt @@ -55,7 +55,7 @@ class DisconnectionQueue { } Repository.playerNames.remove(player.name) Repository.lobbyPlayers.remove(player) - Repository.players.remove(player) + Repository.removePlayer(player) Repository.LOGGED_IN_PLAYERS.remove(player.details.username) SystemLogger.logInfo("Player cleared. Removed ${player.details.username}") try { diff --git a/Server/src/main/kotlin/rs09/game/world/repository/Repository.kt b/Server/src/main/kotlin/rs09/game/world/repository/Repository.kt index b2450e94d..8f4fe3f8b 100644 --- a/Server/src/main/kotlin/rs09/game/world/repository/Repository.kt +++ b/Server/src/main/kotlin/rs09/game/world/repository/Repository.kt @@ -19,6 +19,8 @@ object Repository { */ @JvmStatic val players = NodeList(ServerConstants.MAX_PLAYERS) + val uid_map = HashMap(ServerConstants.MAX_PLAYERS) + /** * Represents the repository of active npcs. */ @@ -129,6 +131,18 @@ object Repository { return null } + @JvmStatic + fun addPlayer(player: Player){ + players.add(player) + uid_map[player.details.uid] = player + } + + @JvmStatic + fun removePlayer(player: Player){ + players.remove(player) + uid_map.remove(player.details.uid) + } + /** * Find a non-player character. * @param npcId The non-player character's id.