Add price checker

This commit is contained in:
nea
2023-05-29 23:28:52 +02:00
parent c133505e3b
commit dd0afac3a8
13 changed files with 285 additions and 29 deletions

View File

@@ -100,6 +100,8 @@ dependencies {
transInclude(nonModImplentation(ktor("client-java"))!!) transInclude(nonModImplentation(ktor("client-java"))!!)
transInclude(nonModImplentation(ktor("serialization-kotlinx-json"))!!) transInclude(nonModImplentation(ktor("serialization-kotlinx-json"))!!)
transInclude(nonModImplentation(ktor("client-content-negotiation"))!!) transInclude(nonModImplentation(ktor("client-content-negotiation"))!!)
transInclude(nonModImplentation(ktor("client-encoding"))!!)
transInclude(nonModImplentation(ktor("client-logging"))!!)
// Dev environment preinstalled mods // Dev environment preinstalled mods
modRuntimeOnly(libs.bundles.runtime.required) modRuntimeOnly(libs.bundles.runtime.required)

View File

@@ -11,7 +11,7 @@ modmenu = "6.2.1"
ktor = "2.3.0" ktor = "2.3.0"
dbus_java = "4.2.1" dbus_java = "4.2.1"
architectury = "8.1.79" architectury = "8.1.79"
neurepoparser = "0.0.1" neurepoparser = "1.1.0"
qolify = "1.2.2-1.19.4" qolify = "1.2.2-1.19.4"
citresewn = "1.1.3+1.19.4" citresewn = "1.1.3+1.19.4"
ncr = "Fabric-1.19.4-v2.1.1" ncr = "Fabric-1.19.4-v2.1.1"

View File

@@ -19,11 +19,14 @@
package moe.nea.firmament package moe.nea.firmament
import com.mojang.brigadier.CommandDispatcher import com.mojang.brigadier.CommandDispatcher
import io.ktor.client.* import io.ktor.client.HttpClient
import io.ktor.client.plugins.* import io.ktor.client.plugins.UserAgent
import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.plugins.cache.HttpCache
import io.ktor.serialization.kotlinx.json.* import io.ktor.client.plugins.compression.ContentEncoding
import java.awt.Taskbar.Feature import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logging
import io.ktor.serialization.kotlinx.json.json
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import net.fabricmc.api.ClientModInitializer import net.fabricmc.api.ClientModInitializer
@@ -35,14 +38,21 @@ import net.fabricmc.loader.api.FabricLoader
import net.fabricmc.loader.api.Version import net.fabricmc.loader.api.Version
import net.fabricmc.loader.api.metadata.ModMetadata import net.fabricmc.loader.api.metadata.ModMetadata
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger
import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.plus
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
import net.minecraft.command.CommandRegistryAccess import net.minecraft.command.CommandRegistryAccess
import moe.nea.firmament.commands.registerFirmamentCommand import moe.nea.firmament.commands.registerFirmamentCommand
import moe.nea.firmament.dbus.FirmamentDbusObject import moe.nea.firmament.dbus.FirmamentDbusObject
import moe.nea.firmament.features.FeatureManager import moe.nea.firmament.features.FeatureManager
import moe.nea.firmament.repo.ItemCostData
import moe.nea.firmament.repo.RepoManager import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.SBData import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.data.IDataHolder import moe.nea.firmament.util.data.IDataHolder
@@ -53,8 +63,10 @@ object Firmament : ModInitializer, ClientModInitializer {
val DEBUG = System.getProperty("firmament.debug") == "true" val DEBUG = System.getProperty("firmament.debug") == "true"
val DATA_DIR: Path = Path.of(".firmament").also { Files.createDirectories(it) } val DATA_DIR: Path = Path.of(".firmament").also { Files.createDirectories(it) }
val CONFIG_DIR: Path = Path.of("config/firmament").also { Files.createDirectories(it) } val CONFIG_DIR: Path = Path.of("config/firmament").also { Files.createDirectories(it) }
val logger = LogManager.getLogger("Firmament") val logger: Logger = LogManager.getLogger("Firmament")
val metadata: ModMetadata by lazy { FabricLoader.getInstance().getModContainer(MOD_ID).orElseThrow().metadata } private val metadata: ModMetadata by lazy {
FabricLoader.getInstance().getModContainer(MOD_ID).orElseThrow().metadata
}
val version: Version by lazy { metadata.version } val version: Version by lazy { metadata.version }
val json = Json { val json = Json {
@@ -68,9 +80,18 @@ object Firmament : ModInitializer, ClientModInitializer {
install(ContentNegotiation) { install(ContentNegotiation) {
json(json) json(json)
} }
install(ContentEncoding) {
gzip()
deflate()
}
install(UserAgent) { install(UserAgent) {
agent = "Firmament/$version" agent = "Firmament/$version"
} }
if (DEBUG)
install(Logging) {
level = LogLevel.INFO
}
install(HttpCache)
} }
} }
@@ -98,7 +119,7 @@ object Firmament : ModInitializer, ClientModInitializer {
RepoManager.initialize() RepoManager.initialize()
SBData.init() SBData.init()
FeatureManager.autoload() FeatureManager.autoload()
ItemCostData.spawnPriceLoop()
ClientCommandRegistrationCallback.EVENT.register(this::registerCommands) ClientCommandRegistrationCallback.EVENT.register(this::registerCommands)
ClientLifecycleEvents.CLIENT_STOPPING.register(ClientLifecycleEvents.ClientStopping { ClientLifecycleEvents.CLIENT_STOPPING.register(ClientLifecycleEvents.ClientStopping {
runBlocking { runBlocking {

View File

@@ -23,6 +23,7 @@ import com.mojang.brigadier.builder.ArgumentBuilder
import com.mojang.brigadier.builder.LiteralArgumentBuilder import com.mojang.brigadier.builder.LiteralArgumentBuilder
import com.mojang.brigadier.builder.RequiredArgumentBuilder import com.mojang.brigadier.builder.RequiredArgumentBuilder
import com.mojang.brigadier.context.CommandContext import com.mojang.brigadier.context.CommandContext
import com.mojang.brigadier.suggestion.SuggestionProvider
import java.lang.reflect.ParameterizedType import java.lang.reflect.ParameterizedType
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
import moe.nea.firmament.util.iterate import moe.nea.firmament.util.iterate
@@ -80,6 +81,17 @@ fun <T : ArgumentBuilder<DefaultSource, T>, AT : Any> T.thenArgument(
block: RequiredArgumentBuilder<DefaultSource, AT>.(TypeSafeArg<AT>) -> Unit block: RequiredArgumentBuilder<DefaultSource, AT>.(TypeSafeArg<AT>) -> Unit
): T = then(argument(name, argument, block)) ): T = then(argument(name, argument, block))
fun <T : RequiredArgumentBuilder<DefaultSource, String>> T.suggestsList(provider: () -> Iterable<String>) {
suggests(SuggestionProvider<DefaultSource> { context, builder ->
provider()
.asSequence()
.filter { it.startsWith(builder.remaining, ignoreCase = true) }
.forEach {
builder.suggest(it)
}
builder.buildFuture()
})
}
fun <T : ArgumentBuilder<DefaultSource, T>> T.thenLiteral( fun <T : ArgumentBuilder<DefaultSource, T>> T.thenLiteral(
name: String, name: String,

View File

@@ -19,12 +19,16 @@
package moe.nea.firmament.commands package moe.nea.firmament.commands
import com.mojang.brigadier.CommandDispatcher import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.arguments.StringArgumentType.getString
import com.mojang.brigadier.arguments.StringArgumentType.string
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
import net.minecraft.text.Text import net.minecraft.text.Text
import moe.nea.firmament.features.world.FairySouls import moe.nea.firmament.features.world.FairySouls
import moe.nea.firmament.gui.config.AllConfigsGui import moe.nea.firmament.gui.config.AllConfigsGui
import moe.nea.firmament.repo.ItemCostData
import moe.nea.firmament.repo.RepoManager import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.SBData import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.SkyblockId
fun firmamentCommand() = literal("firmament") { fun firmamentCommand() = literal("firmament") {
@@ -47,6 +51,28 @@ fun firmamentCommand() = literal("firmament") {
} }
} }
} }
thenLiteral("price") {
thenArgument("item", string()) { item ->
suggestsList { RepoManager.neuRepo.items.items.keys }
thenExecute {
val itemName = SkyblockId(getString(context, "item"))
source.sendFeedback(Text.translatable("firmament.price", itemName.neuItem))
val bazaarData = ItemCostData.bazaarData[itemName]
if (bazaarData != null) {
source.sendFeedback(Text.translatable("firmament.price.bazaar"))
source.sendFeedback(Text.translatable("firmament.price.bazaar.productid", bazaarData.productId.bazaarId))
source.sendFeedback(Text.translatable("firmament.price.bazaar.buy.price", bazaarData.quickStatus.buyPrice))
source.sendFeedback(Text.translatable("firmament.price.bazaar.buy.order", bazaarData.quickStatus.buyOrders))
source.sendFeedback(Text.translatable("firmament.price.bazaar.sell.price", bazaarData.quickStatus.sellPrice))
source.sendFeedback(Text.translatable("firmament.price.bazaar.sell.order", bazaarData.quickStatus.sellOrders))
}
val lowestBin = ItemCostData.lowestBin[itemName]
if (lowestBin != null) {
source.sendFeedback(Text.translatable("firmament.price.lowestbin", lowestBin))
}
}
}
}
thenLiteral("dev") { thenLiteral("dev") {
thenLiteral("config") { thenLiteral("config") {
thenExecute { thenExecute {

View File

@@ -19,10 +19,16 @@
package moe.nea.firmament.events package moe.nea.firmament.events
import net.minecraft.client.option.KeyBinding import net.minecraft.client.option.KeyBinding
import moe.nea.firmament.keybindings.IKeyBinding
data class HandledScreenKeyPressedEvent(val keyCode: Int, val scanCode: Int, val modifiers: Int) : FirmamentEvent.Cancellable() { data class HandledScreenKeyPressedEvent(val keyCode: Int, val scanCode: Int, val modifiers: Int) : FirmamentEvent.Cancellable() {
companion object : FirmamentEventBus<HandledScreenKeyPressedEvent>() companion object : FirmamentEventBus<HandledScreenKeyPressedEvent>()
fun matches(keyBinding: KeyBinding): Boolean { fun matches(keyBinding: KeyBinding): Boolean {
return keyBinding.matchesKey(keyCode, scanCode) return matches(IKeyBinding.minecraft(keyBinding))
}
fun matches(keyBinding: IKeyBinding): Boolean {
return keyBinding.matches(keyCode, scanCode, modifiers)
} }
} }

View File

@@ -0,0 +1,15 @@
package moe.nea.firmament.events
import net.minecraft.client.gui.tooltip.Tooltip
import net.minecraft.client.item.TooltipContext
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.item.ItemStack
data class TooltipEvent(
val itemStack: ItemStack,
val tooltip: Tooltip,
val tooltipContext: TooltipContext,
val player: PlayerEntity?
) : FirmamentEvent() {
companion object : FirmamentEventBus<TooltipEvent>()
}

View File

@@ -0,0 +1,27 @@
package moe.nea.firmament.keybindings
import net.minecraft.client.option.KeyBinding
interface IKeyBinding {
fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean
fun withModifiers(wantedModifiers: Int): IKeyBinding {
val old = this
return object : IKeyBinding {
override fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
return old.matches(keyCode, scanCode, modifiers) && (modifiers and wantedModifiers) == wantedModifiers
}
}
}
companion object {
fun minecraft(keyBinding: KeyBinding) = object : IKeyBinding {
override fun matches(keyCode: Int, scanCode: Int, modifiers: Int) =
keyBinding.matchesKey(keyCode, scanCode)
}
fun ofKeyCode(wantedKeyCode: Int) = object : IKeyBinding {
override fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean = keyCode == wantedKeyCode
}
}
}

View File

@@ -106,7 +106,7 @@ object SBItemEntryDefinition : EntryDefinition<SBItemStack> {
getEntry(SBItemStack(skyblockId, RepoManager.getNEUItem(skyblockId), count)) getEntry(SBItemStack(skyblockId, RepoManager.getNEUItem(skyblockId), count))
fun getEntry(ingredient: NEUIngredient): EntryStack<SBItemStack> = fun getEntry(ingredient: NEUIngredient): EntryStack<SBItemStack> =
getEntry(SkyblockId(ingredient.itemId), count = ingredient.amount) getEntry(SkyblockId(ingredient.itemId), count = ingredient.amount.toInt())
} }

View File

@@ -0,0 +1,86 @@
package moe.nea.firmament.repo
import io.ktor.client.call.body
import io.ktor.client.request.get
import org.apache.logging.log4j.LogManager
import org.lwjgl.glfw.GLFW
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlin.time.Duration.Companion.minutes
import moe.nea.firmament.Firmament
import moe.nea.firmament.keybindings.IKeyBinding
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.async.waitForInput
object ItemCostData {
private val logger = LogManager.getLogger("Firmament.ItemCostData")
private val moulberryBaseUrl = "https://moulberry.codes"
private val hypixelApiBaseUrl = "https://api.hypixel.net"
var lowestBin: Map<SkyblockId, Double> = mapOf()
private set
var bazaarData: Map<SkyblockId, BazaarData> = mapOf()
private set
@Serializable
data class BazaarData(
@SerialName("product_id")
val productId: SkyblockId.BazaarStock,
@SerialName("quick_status")
val quickStatus: BazaarStatus,
)
@Serializable
data class BazaarStatus(
val sellPrice: Double,
val sellVolume: Long,
val sellMovingWeek: Long,
val sellOrders: Long,
val buyPrice: Double,
val buyVolume: Long,
val buyMovingWeek: Long,
val buyOrders: Long
)
@Serializable
private data class BazaarResponse(
val success: Boolean,
val products: Map<SkyblockId.BazaarStock, BazaarData> = mapOf(),
)
fun getPriceOfItem(item: SkyblockId): Double? = bazaarData[item]?.quickStatus?.buyPrice ?: lowestBin[item]
fun spawnPriceLoop() {
Firmament.coroutineScope.launch {
while (true) {
logger.info("Updating NEU prices")
updatePrices()
withTimeoutOrNull(10.minutes) { waitForInput(IKeyBinding.ofKeyCode(GLFW.GLFW_KEY_U)) }
}
}
}
private suspend fun updatePrices() {
awaitAll(
Firmament.coroutineScope.async { fetchBazaarPrices() },
Firmament.coroutineScope.async { fetchPricesFromMoulberry() },
)
}
private suspend fun fetchPricesFromMoulberry() {
lowestBin = Firmament.httpClient.get("$moulberryBaseUrl/lowestbin.json")
.body<Map<SkyblockId, Double>>()
}
private suspend fun fetchBazaarPrices() {
val response = Firmament.httpClient.get("$hypixelApiBaseUrl/skyblock/bazaar").body<BazaarResponse>()
if (!response.success) {
logger.warn("Retrieved unsuccessful bazaar data")
}
bazaarData = response.products.mapKeys { it.key.toRepoId() }
}
}

View File

@@ -27,11 +27,36 @@ import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtCompound import net.minecraft.nbt.NbtCompound
import net.minecraft.util.Identifier import net.minecraft.util.Identifier
/**
* A skyblock item id, as used by the NEU repo.
* This is not exactly the format used by HyPixel, but is mostly the same.
* Usually this id splits an id used by HyPixel into more sub items. For example `PET` becomes `$PET_ID;$PET_RARITY`,
* with those values extracted from other metadata.
*/
@JvmInline @JvmInline
@Serializable
value class SkyblockId(val neuItem: String) { value class SkyblockId(val neuItem: String) {
val identifier get() = Identifier("skyblockitem", neuItem.lowercase().replace(";", "__")) val identifier get() = Identifier("skyblockitem", neuItem.lowercase().replace(";", "__"))
/**
* A bazaar stock item id, as returned by the HyPixel bazaar api endpoint.
* These are not equivalent to the in-game ids, or the NEU repo ids, and in fact, do not refer to items, but instead
* to bazaar stocks. The main difference from [SkyblockId]s is concerning enchanted books. There are probably more,
* but for now this holds.
*/
@JvmInline
@Serializable
value class BazaarStock(val bazaarId: String) {
fun toRepoId(): SkyblockId {
bazaarEnchantmentRegex.matchEntire(bazaarId)?.let {
return SkyblockId("${it.groupValues[1]};${it.groupValues[2]}")
}
return SkyblockId(bazaarId.replace(":", "-"))
}
}
companion object { companion object {
private val bazaarEnchantmentRegex = "ENCHANTMENT_(\\D*)_(\\d+)".toRegex()
val NULL: SkyblockId = SkyblockId("null") val NULL: SkyblockId = SkyblockId("null")
} }
} }

View File

@@ -0,0 +1,45 @@
package moe.nea.firmament.util.async
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import moe.nea.firmament.events.HandledScreenKeyPressedEvent
import moe.nea.firmament.keybindings.IKeyBinding
private object InputHandler {
data class KeyInputContinuation(val keybind: IKeyBinding, val onContinue: () -> Unit)
private val activeContinuations = mutableListOf<KeyInputContinuation>()
fun registerContinuation(keyInputContinuation: KeyInputContinuation): () -> Unit {
synchronized(InputHandler) {
activeContinuations.add(keyInputContinuation)
}
return {
synchronized(this) {
activeContinuations.remove(keyInputContinuation)
}
}
}
init {
HandledScreenKeyPressedEvent.subscribe { event ->
synchronized(InputHandler) {
val toRemove = activeContinuations.filter {
event.matches(it.keybind)
}
toRemove.forEach { it.onContinue() }
activeContinuations.removeAll(toRemove)
}
}
}
}
suspend fun waitForInput(keybind: IKeyBinding): Unit = suspendCancellableCoroutine { cont ->
val unregister =
InputHandler.registerContinuation(InputHandler.KeyInputContinuation(keybind) { cont.resume(Unit) })
cont.invokeOnCancellation {
unregister()
}
}

View File

@@ -1,21 +1,12 @@
/**
* Firmament is a Hypixel Skyblock mod for modern Minecraft versions
* Copyright (C) 2023 Linnea Gräf
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
{ {
"firmament.price": "Checking price for %s",
"firmament.price.bazaar": "Bazaar stats:",
"firmament.price.bazaar.productid": "Stock id: %s",
"firmament.price.bazaar.buy.price": "Buy Price: %.1f",
"firmament.price.bazaar.buy.order": "Buy orders: %d",
"firmament.price.bazaar.sell.price": "Sell Price: %.1f",
"firmament.price.bazaar.sell.order": "Sell orders: %d",
"firmament.price.lowestbin": "Lowest BIN: %.1f",
"firmament.repo.reload.network": "Trying to redownload the repository", "firmament.repo.reload.network": "Trying to redownload the repository",
"firmament.repo.reload.disk": "Reloading repository from disk. This may lag a bit.", "firmament.repo.reload.disk": "Reloading repository from disk. This may lag a bit.",
"firmament.repo.cache": "Recaching items", "firmament.repo.cache": "Recaching items",