Majority of GE work

This commit is contained in:
ceikry 2022-03-10 20:55:44 -06:00
parent 609702ec27
commit f36faa0f09
10 changed files with 450 additions and 29 deletions

View file

@ -32,7 +32,8 @@ dependencies {
"libs/classgraph-4.8.98.jar", "libs/classgraph-4.8.98.jar",
"libs/mysql-connector-java-8.0.21.jar", "libs/mysql-connector-java-8.0.21.jar",
"libs/mordant-jvm-2.0.0-alpha2.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"
) )
} }

View file

@ -12,6 +12,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import rs09.game.content.global.GlobalKillCounter; import rs09.game.content.global.GlobalKillCounter;
import rs09.game.ge.GEAutoStock import rs09.game.ge.GEAutoStock
import rs09.game.ge.GEDB
import rs09.game.system.SystemLogger import rs09.game.system.SystemLogger
import rs09.game.system.config.ServerConfigParser import rs09.game.system.config.ServerConfigParser
import rs09.game.world.GameWorld import rs09.game.world.GameWorld
@ -64,6 +65,7 @@ object Server {
SystemLogger.logInfo("Using config file: ${"worldprops" + File.separator + "default.conf"}") SystemLogger.logInfo("Using config file: ${"worldprops" + File.separator + "default.conf"}")
ServerConfigParser.parse("worldprops" + File.separator + "default.conf") ServerConfigParser.parse("worldprops" + File.separator + "default.conf")
} }
GEDB.init()
startTime = System.currentTimeMillis() startTime = System.currentTimeMillis()
val t = TimeStamp() val t = TimeStamp()
SystemLogger.logInfo("Initializing Server Store...") SystemLogger.logInfo("Initializing Server Store...")

View file

@ -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})")
}
}
}
}

View file

@ -1,7 +1,15 @@
package rs09.game.ge 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 core.game.world.callback.CallBack
import rs09.game.system.SystemLogger import rs09.game.system.SystemLogger
import rs09.game.world.repository.Repository
import rs09.tools.secondsToTicks import rs09.tools.secondsToTicks
object GrandExchange : CallBack { object GrandExchange : CallBack {
@ -14,15 +22,12 @@ object GrandExchange : CallBack {
* Initializes the offer manager and spawns an update thread. * 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. * @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(isRunning) return
if(!local){
TODO("Remote GE server stuff")
}
SystemLogger.logGE("Initializing GE...") SystemLogger.logGE("Initializing GE...")
OfferManager.init() //OfferManager.init()
SystemLogger.logGE("GE Initialized.") SystemLogger.logGE("GE Initialized.")
SystemLogger.logGE("Initializing GE Update Worker") SystemLogger.logGE("Initializing GE Update Worker")
@ -31,11 +36,39 @@ object GrandExchange : CallBack {
Thread.currentThread().name = "GE Update Worker" Thread.currentThread().name = "GE Update Worker"
while(true) { while(true) {
SystemLogger.logGE("Updating offers...") SystemLogger.logGE("Updating offers...")
OfferManager.update() val conn = GEDB.connect()
if(OfferManager.dumpDatabase){ val stmt = conn.createStatement()
SystemLogger.logGE("Saving GE...") val buy_offer = stmt.executeQuery("SELECT * from player_offers where is_sale = 0")
OfferManager.save()
OfferManager.dumpDatabase = false 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 Thread.sleep(60_000) //sleep for 60 seconds
} }
@ -44,8 +77,68 @@ object GrandExchange : CallBack {
isRunning = true 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 { override fun call(): Boolean {
boot(true) GEDB.init()
boot()
return true return true
} }
} }

View file

@ -4,12 +4,19 @@ import core.cache.def.impl.ItemDefinition
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.item.Item 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() { class GrandExchangeOffer() {
@ -27,6 +34,7 @@ class GrandExchangeOffer() {
var player: Player? = null var player: Player? = null
var playerUID = 0 var playerUID = 0
var isLimitation = false var isLimitation = false
var isBot = false
/** /**
* Gets the total amount of money entered. * Gets the total amount of money entered.
@ -49,7 +57,164 @@ class GrandExchangeOffer() {
val isActive: Boolean val isActive: Boolean
get() = offerState != OfferState.ABORTED && offerState != OfferState.PENDING && offerState != OfferState.COMPLETED && offerState != OfferState.REMOVED 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 { 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 + "]" 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
}
}
} }

View file

@ -342,16 +342,13 @@ object OfferManager {
fun dispatch(player: Player, offer: GrandExchangeOffer): Boolean { fun dispatch(player: Player, offer: GrandExchangeOffer): Boolean {
if (offer.amount < 1) { if (offer.amount < 1) {
player.packetDispatch.sendMessage("You must choose the quantity you wish to buy!") player.packetDispatch.sendMessage("You must choose the quantity you wish to buy!")
println("amountthing")
return false return false
} }
if (offer.offeredValue < 1) { if (offer.offeredValue < 1) {
player.packetDispatch.sendMessage("You must choose the price you wish to buy for!") player.packetDispatch.sendMessage("You must choose the price you wish to buy for!")
println("pricethng")
return false return false
} }
if (offer.offerState != OfferState.PENDING || offer.uid != 0L) { if (offer.offerState != OfferState.PENDING || offer.uid != 0L) {
println("pendingthing")
return false return false
} }
if (player.isArtificial) { if (player.isArtificial) {

View file

@ -296,13 +296,7 @@ class PlayerGrandExchange(private val player: Player) {
* @param offer The offer to update. * @param offer The offer to update.
*/ */
fun update(offer: GrandExchangeOffer?) { fun update(offer: GrandExchangeOffer?) {
if (offer != null) { offer?.visualize(player)
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)
)
}
} }
/** /**

View file

@ -106,7 +106,7 @@ class LoginParser(
return return
} }
if(!PlayerParser.parse(player)){ if(!PlayerParser.parse(player)){
Repository.players.remove(player) Repository.removePlayer(player)
Repository.LOGGED_IN_PLAYERS.remove(player.username) Repository.LOGGED_IN_PLAYERS.remove(player.username)
Repository.lobbyPlayers.remove(player) Repository.lobbyPlayers.remove(player)
Repository.playerNames.remove(player.name) Repository.playerNames.remove(player.name)
@ -124,10 +124,10 @@ class LoginParser(
p.clear() p.clear()
Repository.playerNames.remove(p.name) Repository.playerNames.remove(p.name)
Repository.lobbyPlayers.remove(p) Repository.lobbyPlayers.remove(p)
Repository.players.remove(p) Repository.removePlayer(p)
} }
if (!Repository.players.contains(player)) { if (!Repository.players.contains(player)) {
Repository.players.add(player) Repository.addPlayer(player)
} }
player.details.session.setObject(player) player.details.session.setObject(player)
flag(Response.SUCCESSFUL) flag(Response.SUCCESSFUL)
@ -179,7 +179,7 @@ class LoginParser(
GameWorld.Pulser.submit(object : Pulse(1) { GameWorld.Pulser.submit(object : Pulse(1) {
override fun pulse(): Boolean { override fun pulse(): Boolean {
if (!Repository.players.contains(player)) { if (!Repository.players.contains(player)) {
Repository.players.add(player) Repository.addPlayer(player)
} }
return true return true
} }

View file

@ -55,7 +55,7 @@ class DisconnectionQueue {
} }
Repository.playerNames.remove(player.name) Repository.playerNames.remove(player.name)
Repository.lobbyPlayers.remove(player) Repository.lobbyPlayers.remove(player)
Repository.players.remove(player) Repository.removePlayer(player)
Repository.LOGGED_IN_PLAYERS.remove(player.details.username) Repository.LOGGED_IN_PLAYERS.remove(player.details.username)
SystemLogger.logInfo("Player cleared. Removed ${player.details.username}") SystemLogger.logInfo("Player cleared. Removed ${player.details.username}")
try { try {

View file

@ -19,6 +19,8 @@ object Repository {
*/ */
@JvmStatic @JvmStatic
val players = NodeList<Player>(ServerConstants.MAX_PLAYERS) val players = NodeList<Player>(ServerConstants.MAX_PLAYERS)
val uid_map = HashMap<Int,Player>(ServerConstants.MAX_PLAYERS)
/** /**
* Represents the repository of active npcs. * Represents the repository of active npcs.
*/ */
@ -129,6 +131,18 @@ object Repository {
return null 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. * Find a non-player character.
* @param npcId The non-player character's id. * @param npcId The non-player character's id.