/*
* Copyright (C) 2022 NotEnoughUpdates contributors
*
* This file is part of NotEnoughUpdates.
*
* NotEnoughUpdates is free software: you can redistribute it
* and/or modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation, either
* version 3 of the License, or (at your option) any later version.
*
* NotEnoughUpdates 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with NotEnoughUpdates. If not, see .
*/
package io.github.moulberry.notenoughupdates.auction;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import io.github.moulberry.notenoughupdates.ItemPriceInformation;
import io.github.moulberry.notenoughupdates.NEUManager;
import io.github.moulberry.notenoughupdates.miscgui.pricegraph.LocalGraphDataProvider;
import io.github.moulberry.notenoughupdates.recipes.Ingredient;
import io.github.moulberry.notenoughupdates.recipes.ItemShopRecipe;
import io.github.moulberry.notenoughupdates.recipes.NeuRecipe;
import io.github.moulberry.notenoughupdates.util.Utils;
import net.minecraft.item.Item;
import net.minecraft.util.ResourceLocation;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class APIManager {
private final NEUManager manager;
private final int LOWEST_BIN_UPDATE_INTERVAL = 2 * 60 * 1000; // 2 minutes
private final int AUCTION_AVG_UPDATE_INTERVAL = 5 * 60 * 1000; // 5 minutes
private final int BAZAAR_UPDATE_INTERVAL = 5 * 60 * 1000; // 5 minutes
private JsonObject lowestBins = null;
private JsonObject auctionPricesAvgLowestBinJson = null;
private JsonObject bazaarJson = null;
private JsonObject auctionPricesJson = null;
private final HashMap craftCost = new HashMap<>();
private boolean didFirstUpdate = false;
private long lastAuctionAvgUpdate = 0;
private long lastBazaarUpdate = 0;
private long lastLowestBinUpdate = 0;
public APIManager(NEUManager manager) {
this.manager = manager;
}
public void tick() {
long currentTime = System.currentTimeMillis();
if (currentTime - lastAuctionAvgUpdate > AUCTION_AVG_UPDATE_INTERVAL) {
lastAuctionAvgUpdate = currentTime - AUCTION_AVG_UPDATE_INTERVAL + 60 * 1000; // Try again in 1 minute on failure
updateAvgPrices();
}
if (currentTime - lastBazaarUpdate > BAZAAR_UPDATE_INTERVAL) {
lastBazaarUpdate = currentTime - BAZAAR_UPDATE_INTERVAL + 60 * 1000; // Try again in 1 minute on failure
updateBazaar();
}
if (currentTime - lastLowestBinUpdate > LOWEST_BIN_UPDATE_INTERVAL) {
lastLowestBinUpdate = currentTime - LOWEST_BIN_UPDATE_INTERVAL + 30 * 1000; // Try again in 30 seconds on failure
updateLowestBin();
}
}
public Set getLowestBinKeySet() {
if (lowestBins == null) return new HashSet<>();
HashSet keys = new HashSet<>();
for (Map.Entry entry : lowestBins.entrySet()) {
keys.add(entry.getKey());
}
return keys;
}
public long getLowestBin(String internalName) {
if (lowestBins != null && lowestBins.has(internalName)) {
JsonElement e = lowestBins.get(internalName);
if (e.isJsonPrimitive() && e.getAsJsonPrimitive().isNumber()) {
return e.getAsLong();
}
}
return -1;
}
public void updateLowestBin() {
manager.apiUtils
.newMoulberryRequest("lowestbin.json.gz")
.gunzip()
.requestJson()
.thenAcceptAsync(jsonObject -> {
if (lowestBins == null) {
lowestBins = new JsonObject();
}
if (!jsonObject.entrySet().isEmpty()) {
lastLowestBinUpdate = System.currentTimeMillis();
}
for (Map.Entry entry : jsonObject.entrySet()) {
lowestBins.add(entry.getKey(), entry.getValue());
}
if (!didFirstUpdate) {
ItemPriceInformation.updateAuctionableItemsList();
didFirstUpdate = true;
}
LocalGraphDataProvider.INSTANCE.savePrices(lowestBins, false);
});
}
public long getLastLowestBinUpdateTime() {
return lastLowestBinUpdate;
}
private static final Pattern BAZAAR_ENCHANTMENT_PATTERN = Pattern.compile("ENCHANTMENT_(\\D*)_(\\d+)");
public String transformHypixelBazaarToNEUItemId(String hypixelId) {
Matcher matcher = BAZAAR_ENCHANTMENT_PATTERN.matcher(hypixelId);
if (matcher.matches()) {
return matcher.group(1) + ";" + matcher.group(2);
}
return hypixelId.replace(":", "-");
}
public void updateBazaar() {
manager.apiUtils
.newAnonymousHypixelApiRequest("skyblock/bazaar")
.requestJson()
.thenAcceptAsync(jsonObject -> {
if (!jsonObject.get("success").getAsBoolean()) return;
craftCost.clear();
bazaarJson = new JsonObject();
JsonObject products = jsonObject.get("products").getAsJsonObject();
for (Map.Entry entry : products.entrySet()) {
if (entry.getValue().isJsonObject()) {
JsonObject productInfo = new JsonObject();
JsonObject product = entry.getValue().getAsJsonObject();
JsonObject quickStatus = product.getAsJsonObject("quick_status");
if (!hasData(quickStatus)) continue;
productInfo.addProperty("avg_buy", quickStatus.get("buyPrice").getAsFloat());
productInfo.addProperty("avg_sell", quickStatus.get("sellPrice").getAsFloat());
float instasellsWeekly = quickStatus.get("sellMovingWeek").getAsFloat();
float instabuysWeekly = quickStatus.get("buyMovingWeek").getAsFloat();
productInfo.addProperty("instasells_weekly", instasellsWeekly);
productInfo.addProperty("instabuys_weekly", instabuysWeekly);
productInfo.addProperty("instasells_daily", instasellsWeekly / 7);
productInfo.addProperty("instabuys_daily", instabuysWeekly / 7);
productInfo.addProperty("instasells_hourly", instasellsWeekly / 7 / 24);
productInfo.addProperty("instabuys_hourly", instabuysWeekly / 7 / 24);
for (JsonElement element : product.get("sell_summary").getAsJsonArray()) {
if (element.isJsonObject()) {
JsonObject sellSummaryFirst = element.getAsJsonObject();
productInfo.addProperty("curr_sell", sellSummaryFirst.get("pricePerUnit").getAsFloat());
break;
}
}
for (JsonElement element : product.get("buy_summary").getAsJsonArray()) {
if (element.isJsonObject()) {
JsonObject sellSummaryFirst = element.getAsJsonObject();
productInfo.addProperty("curr_buy", sellSummaryFirst.get("pricePerUnit").getAsFloat());
break;
}
}
bazaarJson.add(transformHypixelBazaarToNEUItemId(entry.getKey()), productInfo);
}
}
LocalGraphDataProvider.INSTANCE.savePrices(bazaarJson, true);
});
}
private static boolean hasData(JsonObject quickStatus) {
for (Map.Entry e : quickStatus.entrySet()) {
String key = e.getKey();
if (!key.equals("productId")) {
double value = e.getValue().getAsDouble();
if (value != 0) {
return true;
}
}
}
return false;
}
public void updateAvgPrices() {
manager.apiUtils
.newMoulberryRequest("auction_averages/3day.json.gz")
.gunzip().requestJson().thenAcceptAsync((jsonObject) -> {
craftCost.clear();
auctionPricesJson = jsonObject;
lastAuctionAvgUpdate = System.currentTimeMillis();
});
manager.apiUtils
.newMoulberryRequest("auction_averages_lbin/1day.json.gz")
.gunzip().requestJson()
.thenAcceptAsync((jsonObject) -> {
auctionPricesAvgLowestBinJson = jsonObject;
});
}
public Set getItemAuctionInfoKeySet() {
if (auctionPricesJson == null) return new HashSet<>();
HashSet keys = new HashSet<>();
for (Map.Entry entry : auctionPricesJson.entrySet()) {
keys.add(entry.getKey());
}
return keys;
}
public JsonObject getItemAuctionInfo(String internalname) {
if (auctionPricesJson == null) return null;
JsonElement e = auctionPricesJson.get(internalname);
if (e == null) {
return null;
}
return e.getAsJsonObject();
}
public double getItemAvgBin(String internalName) {
if (auctionPricesAvgLowestBinJson == null) return -1;
JsonElement e = auctionPricesAvgLowestBinJson.get(internalName);
if (e == null) {
return -1;
}
return Math.round(e.getAsDouble());
}
public Set getBazaarKeySet() {
if (bazaarJson == null) return new HashSet<>();
HashSet keys = new HashSet<>();
for (Map.Entry entry : bazaarJson.entrySet()) {
keys.add(entry.getKey());
}
return keys;
}
public double getBazaarOrBin(String internalName, boolean useSellingPrice) {
String curr = (useSellingPrice ? "curr_sell" : "curr_buy");
JsonObject bazaarInfo = manager.auctionManager.getBazaarInfo(internalName);
if (bazaarInfo != null && bazaarInfo.get(curr) != null) {
return bazaarInfo.get(curr).getAsFloat();
} else {
return manager.auctionManager.getLowestBin(internalName);
}
}
public JsonObject getBazaarInfo(String internalName) {
if (bazaarJson == null) return null;
JsonElement e = bazaarJson.get(internalName);
if (e == null) {
return null;
}
return e.getAsJsonObject();
}
public static final List hardcodedVanillaItems = Utils.createList(
"WOOD_AXE", "WOOD_HOE", "WOOD_PICKAXE", "WOOD_SPADE", "WOOD_SWORD",
"GOLD_AXE", "GOLD_HOE", "GOLD_PICKAXE", "GOLD_SPADE", "GOLD_SWORD",
"ROOKIE_HOE"
);
public boolean isVanillaItem(String internalName) {
if (hardcodedVanillaItems.contains(internalName)) return true;
//Removes trailing numbers and underscores, eg. LEAVES_2-3 -> LEAVES
String vanillaName = internalName.split("-")[0];
if (manager.getItemInformation().containsKey(vanillaName)) {
JsonObject json = manager.getItemInformation().get(vanillaName);
if (json != null && json.has("vanilla") && json.get("vanilla").getAsBoolean()) return true;
}
return Item.itemRegistry.getObject(new ResourceLocation(vanillaName)) != null;
}
public static class CraftInfo {
public boolean fromRecipe = false;
public boolean vanillaItem = false;
public double craftCost = -1;
}
public CraftInfo getCraftCost(String internalName) {
return getCraftCost(internalName, new HashSet<>());
}
/**
* Recursively calculates the cost of crafting an item from raw materials.
*/
private CraftInfo getCraftCost(String internalName, Set visited) {
if (craftCost.containsKey(internalName)) return craftCost.get(internalName);
if (visited.contains(internalName)) return null;
visited.add(internalName);
boolean vanillaItem = isVanillaItem(internalName);
double craftCost = Double.POSITIVE_INFINITY;
JsonObject auctionInfo = getItemAuctionInfo(internalName);
double lowestBin = getLowestBin(internalName);
JsonObject bazaarInfo = getBazaarInfo(internalName);
if (bazaarInfo != null && bazaarInfo.get("curr_buy") != null) {
craftCost = bazaarInfo.get("curr_buy").getAsFloat();
}
//Don't use auction prices for vanilla items cuz people like to transfer money, messing up the cost of vanilla items.
if (!vanillaItem) {
if (lowestBin > 0) {
craftCost = Math.min(lowestBin, craftCost);
} else if (auctionInfo != null) {
float auctionPrice = auctionInfo.get("price").getAsFloat() / auctionInfo.get("count").getAsInt();
craftCost = Math.min(auctionPrice, craftCost);
}
}
Set recipes = manager.getRecipesFor(internalName);
boolean fromRecipe = false;
if (recipes != null)
RECIPE_ITER:
for (NeuRecipe recipe : recipes) {
if (recipe instanceof ItemShopRecipe) {
if (vanillaItem) {
continue;
}
}
if (recipe.hasVariableCost() || !recipe.shouldUseForCraftCost()) continue;
float craftPrice = 0;
for (Ingredient i : recipe.getIngredients()) {
if (i.isCoins()) {
craftPrice += i.getCount();
continue;
}
CraftInfo ingredientCraftCost = getCraftCost(i.getInternalItemId(), visited);
if (ingredientCraftCost == null)
continue RECIPE_ITER; // Skip recipes with items further up the chain
craftPrice += ingredientCraftCost.craftCost * i.getCount();
}
int resultCount = 0;
for (Ingredient item : recipe.getOutputs())
if (item.getInternalItemId().equals(internalName))
resultCount += item.getCount();
if (resultCount == 0)
continue;
float craftPricePer = craftPrice / resultCount;
if (craftPricePer < craftCost) {
fromRecipe = true;
craftCost = craftPricePer;
}
}
visited.remove(internalName);
if (Double.isInfinite(craftCost)) {
return null;
}
CraftInfo craftInfo = new CraftInfo();
craftInfo.vanillaItem = vanillaItem;
craftInfo.craftCost = craftCost;
craftInfo.fromRecipe = fromRecipe;
this.craftCost.put(internalName, craftInfo);
return craftInfo;
}
}