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

View file

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

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
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
}
}

View file

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

View file

@ -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) {

View file

@ -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)
}
/**

View file

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

View file

@ -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 {

View file

@ -19,6 +19,8 @@ object Repository {
*/
@JvmStatic
val players = NodeList<Player>(ServerConstants.MAX_PLAYERS)
val uid_map = HashMap<Int,Player>(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.