1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
|
package at.hannibal2.skyhanni.data.bazaar
import at.hannibal2.skyhanni.SkyHanniMod
import at.hannibal2.skyhanni.config.ConfigManager
import at.hannibal2.skyhanni.events.DebugDataCollectEvent
import at.hannibal2.skyhanni.events.LorenzTickEvent
import at.hannibal2.skyhanni.features.inventory.bazaar.BazaarData
import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule
import at.hannibal2.skyhanni.test.command.ErrorManager
import at.hannibal2.skyhanni.utils.APIUtils
import at.hannibal2.skyhanni.utils.ChatUtils
import at.hannibal2.skyhanni.utils.ItemUtils.itemName
import at.hannibal2.skyhanni.utils.LorenzUtils
import at.hannibal2.skyhanni.utils.NEUInternalName
import at.hannibal2.skyhanni.utils.NEUItems
import at.hannibal2.skyhanni.utils.NEUItems.getItemStackOrNull
import at.hannibal2.skyhanni.utils.SimpleTimeMark
import at.hannibal2.skyhanni.utils.json.fromJson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
// https://api.hypixel.net/#tag/SkyBlock/paths/~1v2~1skyblock~1bazaar/get
@SkyHanniModule
object HypixelBazaarFetcher {
private const val URL = "https://api.hypixel.net/v2/skyblock/bazaar"
private const val HIDDEN_FAILED_ATTEMPTS = 3
var latestProductInformation = mapOf<NEUInternalName, BazaarData>()
private var lastSuccessfulFetch = SimpleTimeMark.farPast()
private var nextFetchTime = SimpleTimeMark.farPast()
private var failedAttempts = 0
private var nextFetchIsManual = false
@SubscribeEvent
fun onDebugDataCollect(event: DebugDataCollectEvent) {
event.title("Bazaar Data Fetcher from API")
val data = listOf(
"failedAttempts: $failedAttempts",
"nextFetchIsManual: $nextFetchIsManual",
"nextFetchTime: ${nextFetchTime.timeUntil()}",
"lastSuccessfulFetch: ${lastSuccessfulFetch.passedSince()}",
)
if (failedAttempts == 0) {
event.addIrrelevant(data)
} else {
event.addData(data)
}
}
@SubscribeEvent
fun onTick(event: LorenzTickEvent) {
if (!canFetch()) return
SkyHanniMod.coroutineScope.launch {
fetchAndProcessBazaarData()
}
}
private suspend fun fetchAndProcessBazaarData() {
nextFetchTime = SimpleTimeMark.now() + 2.minutes
val fetchType = if (nextFetchIsManual) "manual" else "automatic"
nextFetchIsManual = false
try {
val jsonResponse = withContext(Dispatchers.IO) { APIUtils.getJSONResponse(URL) }.asJsonObject
val response = ConfigManager.gson.fromJson<BazaarApiResponseJson>(jsonResponse)
if (response.success) {
latestProductInformation = process(response.products)
failedAttempts = 0
lastSuccessfulFetch = SimpleTimeMark.now()
} else {
val rawResponse = jsonResponse.toString()
onError(fetchType, Exception("success=false, cause=${response.cause}"), rawResponse)
}
} catch (e: Exception) {
onError(fetchType, e)
}
}
private fun process(products: Map<String, BazaarProduct>) = products.mapNotNull { (key, product) ->
val internalName = NEUItems.transHypixelNameToInternalName(key)
val sellOfferPrice = product.buySummary.minOfOrNull { it.pricePerUnit } ?: 0.0
val instantBuyPrice = product.sellSummary.maxOfOrNull { it.pricePerUnit } ?: 0.0
if (product.quickStatus.isEmpty()) {
return@mapNotNull null
}
if (internalName.getItemStackOrNull() == null) {
// Items that exist in Hypixel's Bazaar API, but not in NEU repo (not visible in the ingame bazaar).
// Should only include Enchants
if (LorenzUtils.debug) println("Unknown bazaar product: $key/$internalName")
return@mapNotNull null
}
internalName to BazaarData(internalName.itemName, sellOfferPrice, instantBuyPrice, product)
}.toMap()
private fun BazaarQuickStatus.isEmpty(): Boolean = with(this) {
sellPrice == 0.0 &&
sellVolume == 0L &&
sellMovingWeek == 0L &&
sellOrders == 0L &&
buyPrice == 0.0 &&
buyVolume == 0L &&
buyMovingWeek == 0L &&
buyOrders == 0L
}
private fun onError(fetchType: String, e: Exception, rawResponse: String? = null) {
val userMessage = "Failed fetching bazaar price data from hypixel"
failedAttempts++
if (failedAttempts <= HIDDEN_FAILED_ATTEMPTS) {
nextFetchTime = SimpleTimeMark.now() + 15.seconds
ChatUtils.debug("$userMessage. (errorMessage=${e.message}, failedAttempts=$failedAttempts, $fetchType")
e.printStackTrace()
} else {
nextFetchTime = SimpleTimeMark.now() + 15.minutes
if (rawResponse == null || rawResponse.toString() == "{}") {
ChatUtils.chat(
"§cFailed loading Bazaar Price data!\n" +
"Please wait until the Hypixel API is sending correct data again! There is nothing else to do at the moment.",
)
} else {
ErrorManager.logErrorWithData(
e,
userMessage,
"fetchType" to fetchType,
"failedAttempts" to failedAttempts,
"rawResponse" to rawResponse,
)
}
}
}
fun fetchNow() {
failedAttempts = 0
nextFetchIsManual = true
nextFetchTime = SimpleTimeMark.now()
ChatUtils.chat("Manually updating the bazaar prices right now..")
}
private fun canFetch() = LorenzUtils.onHypixel && nextFetchTime.isInPast()
}
|