Add price checker
This commit is contained in:
@@ -100,6 +100,8 @@ dependencies {
|
||||
transInclude(nonModImplentation(ktor("client-java"))!!)
|
||||
transInclude(nonModImplentation(ktor("serialization-kotlinx-json"))!!)
|
||||
transInclude(nonModImplentation(ktor("client-content-negotiation"))!!)
|
||||
transInclude(nonModImplentation(ktor("client-encoding"))!!)
|
||||
transInclude(nonModImplentation(ktor("client-logging"))!!)
|
||||
|
||||
// Dev environment preinstalled mods
|
||||
modRuntimeOnly(libs.bundles.runtime.required)
|
||||
|
||||
@@ -11,7 +11,7 @@ modmenu = "6.2.1"
|
||||
ktor = "2.3.0"
|
||||
dbus_java = "4.2.1"
|
||||
architectury = "8.1.79"
|
||||
neurepoparser = "0.0.1"
|
||||
neurepoparser = "1.1.0"
|
||||
qolify = "1.2.2-1.19.4"
|
||||
citresewn = "1.1.3+1.19.4"
|
||||
ncr = "Fabric-1.19.4-v2.1.1"
|
||||
|
||||
@@ -19,11 +19,14 @@
|
||||
package moe.nea.firmament
|
||||
|
||||
import com.mojang.brigadier.CommandDispatcher
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.plugins.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import java.awt.Taskbar.Feature
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.UserAgent
|
||||
import io.ktor.client.plugins.cache.HttpCache
|
||||
import io.ktor.client.plugins.compression.ContentEncoding
|
||||
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.Path
|
||||
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.metadata.ModMetadata
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.apache.logging.log4j.Logger
|
||||
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 kotlin.coroutines.EmptyCoroutineContext
|
||||
import net.minecraft.command.CommandRegistryAccess
|
||||
import moe.nea.firmament.commands.registerFirmamentCommand
|
||||
import moe.nea.firmament.dbus.FirmamentDbusObject
|
||||
import moe.nea.firmament.features.FeatureManager
|
||||
import moe.nea.firmament.repo.ItemCostData
|
||||
import moe.nea.firmament.repo.RepoManager
|
||||
import moe.nea.firmament.util.SBData
|
||||
import moe.nea.firmament.util.data.IDataHolder
|
||||
@@ -53,8 +63,10 @@ object Firmament : ModInitializer, ClientModInitializer {
|
||||
val DEBUG = System.getProperty("firmament.debug") == "true"
|
||||
val DATA_DIR: Path = Path.of(".firmament").also { Files.createDirectories(it) }
|
||||
val CONFIG_DIR: Path = Path.of("config/firmament").also { Files.createDirectories(it) }
|
||||
val logger = LogManager.getLogger("Firmament")
|
||||
val metadata: ModMetadata by lazy { FabricLoader.getInstance().getModContainer(MOD_ID).orElseThrow().metadata }
|
||||
val logger: Logger = LogManager.getLogger("Firmament")
|
||||
private val metadata: ModMetadata by lazy {
|
||||
FabricLoader.getInstance().getModContainer(MOD_ID).orElseThrow().metadata
|
||||
}
|
||||
val version: Version by lazy { metadata.version }
|
||||
|
||||
val json = Json {
|
||||
@@ -68,9 +80,18 @@ object Firmament : ModInitializer, ClientModInitializer {
|
||||
install(ContentNegotiation) {
|
||||
json(json)
|
||||
}
|
||||
install(ContentEncoding) {
|
||||
gzip()
|
||||
deflate()
|
||||
}
|
||||
install(UserAgent) {
|
||||
agent = "Firmament/$version"
|
||||
}
|
||||
if (DEBUG)
|
||||
install(Logging) {
|
||||
level = LogLevel.INFO
|
||||
}
|
||||
install(HttpCache)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +119,7 @@ object Firmament : ModInitializer, ClientModInitializer {
|
||||
RepoManager.initialize()
|
||||
SBData.init()
|
||||
FeatureManager.autoload()
|
||||
|
||||
ItemCostData.spawnPriceLoop()
|
||||
ClientCommandRegistrationCallback.EVENT.register(this::registerCommands)
|
||||
ClientLifecycleEvents.CLIENT_STOPPING.register(ClientLifecycleEvents.ClientStopping {
|
||||
runBlocking {
|
||||
|
||||
@@ -23,6 +23,7 @@ import com.mojang.brigadier.builder.ArgumentBuilder
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder
|
||||
import com.mojang.brigadier.builder.RequiredArgumentBuilder
|
||||
import com.mojang.brigadier.context.CommandContext
|
||||
import com.mojang.brigadier.suggestion.SuggestionProvider
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
|
||||
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
|
||||
): 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(
|
||||
name: String,
|
||||
|
||||
@@ -19,12 +19,16 @@
|
||||
package moe.nea.firmament.commands
|
||||
|
||||
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.minecraft.text.Text
|
||||
import moe.nea.firmament.features.world.FairySouls
|
||||
import moe.nea.firmament.gui.config.AllConfigsGui
|
||||
import moe.nea.firmament.repo.ItemCostData
|
||||
import moe.nea.firmament.repo.RepoManager
|
||||
import moe.nea.firmament.util.SBData
|
||||
import moe.nea.firmament.util.SkyblockId
|
||||
|
||||
|
||||
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("config") {
|
||||
thenExecute {
|
||||
|
||||
@@ -19,10 +19,16 @@
|
||||
package moe.nea.firmament.events
|
||||
|
||||
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() {
|
||||
companion object : FirmamentEventBus<HandledScreenKeyPressedEvent>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
15
src/main/kotlin/moe/nea/firmament/events/TooltipEvent.kt
Normal file
15
src/main/kotlin/moe/nea/firmament/events/TooltipEvent.kt
Normal 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>()
|
||||
}
|
||||
27
src/main/kotlin/moe/nea/firmament/keybindings/IKeyBinding.kt
Normal file
27
src/main/kotlin/moe/nea/firmament/keybindings/IKeyBinding.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,7 @@ object SBItemEntryDefinition : EntryDefinition<SBItemStack> {
|
||||
getEntry(SBItemStack(skyblockId, RepoManager.getNEUItem(skyblockId), count))
|
||||
|
||||
fun getEntry(ingredient: NEUIngredient): EntryStack<SBItemStack> =
|
||||
getEntry(SkyblockId(ingredient.itemId), count = ingredient.amount)
|
||||
getEntry(SkyblockId(ingredient.itemId), count = ingredient.amount.toInt())
|
||||
|
||||
|
||||
}
|
||||
|
||||
86
src/main/kotlin/moe/nea/firmament/repo/ItemCostData.kt
Normal file
86
src/main/kotlin/moe/nea/firmament/repo/ItemCostData.kt
Normal 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() }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,11 +27,36 @@ import net.minecraft.item.ItemStack
|
||||
import net.minecraft.nbt.NbtCompound
|
||||
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
|
||||
@Serializable
|
||||
value class SkyblockId(val neuItem: String) {
|
||||
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 {
|
||||
private val bazaarEnchantmentRegex = "ENCHANTMENT_(\\D*)_(\\d+)".toRegex()
|
||||
val NULL: SkyblockId = SkyblockId("null")
|
||||
}
|
||||
}
|
||||
|
||||
45
src/main/kotlin/moe/nea/firmament/util/async/input.kt
Normal file
45
src/main/kotlin/moe/nea/firmament/util/async/input.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.disk": "Reloading repository from disk. This may lag a bit.",
|
||||
"firmament.repo.cache": "Recaching items",
|
||||
|
||||
Reference in New Issue
Block a user