/*
* 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.miscgui;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import io.github.moulberry.notenoughupdates.NotEnoughUpdates;
import io.github.moulberry.notenoughupdates.util.SpecialColour;
import io.github.moulberry.notenoughupdates.util.Utils;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Gui;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.item.ItemStack;
import net.minecraft.util.EnumChatFormatting;
import net.minecraft.util.ResourceLocation;
import org.lwjgl.opengl.GL11;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
public class GuiPriceGraph extends GuiScreen {
private static final Gson GSON = new GsonBuilder().create();
private static final SimpleDateFormat format = new SimpleDateFormat("dd-MM-yyyy");
private final ResourceLocation TEXTURE;
private static final int X_SIZE = 364;
private static final int Y_SIZE = 215;
private ItemData itemData;
private float highestValue;
private long firstTime;
private long lastTime;
private Float lowestValue = null;
private String itemName;
private final String itemId;
private int guiLeft;
private int guiTop;
private ItemStack itemStack = null;
private boolean loaded = false;
/**
* 0 = hour
* 1 = day
* 2 = week
* 3 = all
* 4 = custom
**/
private int mode = NotEnoughUpdates.INSTANCE.config.ahGraph.defaultMode;
private long customStart = 0;
private long customEnd = 0;
private boolean customSelecting = false;
public GuiPriceGraph(String itemId) {
switch (NotEnoughUpdates.INSTANCE.config.ahGraph.graphStyle) {
case 1:
TEXTURE = new ResourceLocation("notenoughupdates:price_graph_gui/price_information_gui_dark.png");
break;
case 2:
TEXTURE = new ResourceLocation("notenoughupdates:price_graph_gui/price_information_gui_phqdark.png");
break;
case 3:
TEXTURE = new ResourceLocation("notenoughupdates:price_graph_gui/price_information_gui_fsr.png");
break;
default:
TEXTURE = new ResourceLocation("notenoughupdates:price_graph_gui/price_information_gui.png");
break;
}
this.itemId = itemId;
if (NotEnoughUpdates.INSTANCE.manager.getItemInformation().containsKey(itemId)) {
JsonObject itemInfo = NotEnoughUpdates.INSTANCE.manager.getItemInformation().get(itemId);
itemName = itemInfo.get("displayname").getAsString();
itemStack = NotEnoughUpdates.INSTANCE.manager.jsonToStack(itemInfo);
}
loadData();
}
@Override
public void drawScreen(int mouseX, int mouseY, float partialTicks) {
guiLeft = (width - X_SIZE) / 2;
guiTop = (height - Y_SIZE) / 2;
Minecraft.getMinecraft().getTextureManager().bindTexture(TEXTURE);
GlStateManager.color(1, 1, 1, 1);
Utils.drawTexturedRect(guiLeft, guiTop, X_SIZE, Y_SIZE,
0, X_SIZE / 512f, 0, Y_SIZE / 512f, GL11.GL_NEAREST
);
Utils.drawTexturedRect(guiLeft + 245, guiTop + 17, 16, 16,
0, 16 / 512f, (mode == 0 ? 215 : 231) / 512f, (mode == 0 ? 231 : 247) / 512f, GL11.GL_NEAREST
);
Utils.drawTexturedRect(guiLeft + 263, guiTop + 17, 16, 16,
16 / 512f, 32 / 512f, (mode == 1 ? 215 : 231) / 512f, (mode == 1 ? 231 : 247) / 512f, GL11.GL_NEAREST
);
Utils.drawTexturedRect(guiLeft + 281, guiTop + 17, 16, 16,
32 / 512f, 48 / 512f, (mode == 2 ? 215 : 231) / 512f, (mode == 2 ? 231 : 247) / 512f, GL11.GL_NEAREST
);
Utils.drawTexturedRect(guiLeft + 299, guiTop + 17, 16, 16,
48 / 512f, 64 / 512f, (mode == 3 ? 215 : 231) / 512f, (mode == 3 ? 231 : 247) / 512f, GL11.GL_NEAREST
);
if (itemName != null && itemStack != null) {
Utils.drawItemStack(itemStack, guiLeft + 16, guiTop + 11);
Utils.drawStringScaledMax(itemName, Minecraft.getMinecraft().fontRendererObj, guiLeft + 35, guiTop + 13, false,
0xffffff, 1.77f, 208
);
}
if (!loaded)
Utils.drawStringCentered("Loading...", Minecraft.getMinecraft().fontRendererObj,
guiLeft + 166, guiTop + 116, false, 0xffffff00
);
else if (
itemData == null || itemData.get() == null || itemData.get().size() <= 1 || lowestValue == null)
Utils.drawStringCentered("No data found.", Minecraft.getMinecraft().fontRendererObj,
guiLeft + 166, guiTop + 116, false, 0xffff0000
);
else {
int graphColor = SpecialColour.specialToChromaRGB(NotEnoughUpdates.INSTANCE.config.ahGraph.graphColor);
int graphColor2 = SpecialColour.specialToChromaRGB(NotEnoughUpdates.INSTANCE.config.ahGraph.graphColor2);
Integer lowestDist = null;
Long lowestDistTime = null;
HashMap secondLineData = new HashMap<>();
for (int i = (itemData.isBz() ? 1 : 0); i >= 0; i--) {
Utils.drawGradientRect(0, guiLeft + 17, guiTop + 35, guiLeft + 315, guiTop + 198,
changeAlpha(i == 0 ? graphColor : graphColor2, 120), changeAlpha(i == 0 ? graphColor : graphColor2, 10)
);
Integer prevX = null;
Integer prevY = null;
for (Long time : itemData.get().keySet()) {
float price = itemData.isBz()
? i == 0 ? itemData.bz.get(time).b : itemData.bz.get(time).s
: itemData.ah.get(time);
int xPos = (int) map(time, firstTime, lastTime, guiLeft + 17, guiLeft + 315);
int yPos = (int) map(price, highestValue + 10d, lowestValue - 10d, guiTop + 35, guiTop + 198);
if (prevX != null) {
Minecraft.getMinecraft().getTextureManager().bindTexture(TEXTURE);
GlStateManager.color(1, 1, 1, 1);
Utils.drawTexturedQuad(
prevX,
prevY,
xPos,
yPos,
xPos,
guiTop + 35,
prevX,
guiTop + 35,
18 / 512f,
19 / 512f,
36 / 512f,
37 / 512f,
GL11.GL_NEAREST
);
if (i == 0) {
Utils.drawLine(prevX, prevY + 0.5f, xPos, yPos + 0.5f, 2, graphColor);
if (itemData.isBz())
Utils.drawLine(
prevX,
secondLineData.get(prevX) + 0.5f,
xPos,
secondLineData.get(xPos) + 0.5f,
2,
graphColor2
);
}
}
if (i == 1)
secondLineData.put(xPos, yPos);
if (mouseX >= guiLeft + 17 && mouseX <= guiLeft + 315 && mouseY >= guiTop + 35 && mouseY <= guiTop + 198) {
int dist = Math.abs(mouseX - xPos);
if (lowestDist == null || dist < lowestDist) {
lowestDist = dist;
lowestDistTime = time;
}
}
prevX = xPos;
prevY = yPos;
}
}
boolean showDays = lastTime - firstTime > 86400;
int prevNum = showDays ? Date.from(Instant.ofEpochSecond(firstTime)).getDate() : Date.from(Instant.ofEpochSecond(
firstTime)).getHours();
long prevXPos = -100;
for (long time = firstTime; time <= lastTime; time += showDays ? 3600 : 60) {
int num = showDays
? Date.from(Instant.ofEpochSecond(time)).getDate()
: Date.from(Instant.ofEpochSecond(time)).getHours();
if (num != prevNum) {
int xPos = (int) map(time, firstTime, lastTime, guiLeft + 17, guiLeft + 315);
if (Math.abs(prevXPos - xPos) > 30) {
Utils.drawStringCentered(String.valueOf(num), Minecraft.getMinecraft().fontRendererObj,
xPos, guiTop + 206, false, 0x8b8b8b
);
prevXPos = xPos;
}
prevNum = num;
}
}
for (int i = 0; i <= 6; i++) {
long price = (long) map(i, 0, 6, highestValue, lowestValue);
String formattedPrice = formatPrice(price);
Utils.drawStringF(formattedPrice, Minecraft.getMinecraft().fontRendererObj, guiLeft + 320,
(float) map(i, 0, 6, guiTop + 35, guiTop + 198)
- Minecraft.getMinecraft().fontRendererObj.FONT_HEIGHT / 2f,
false, 0x8b8b8b
);
}
if (customSelecting) {
Utils.drawDottedLine(customStart, guiTop + 36, customStart, guiTop + 197, 2, 10, 0xFFc6c6c6);
Utils.drawDottedLine(customEnd, guiTop + 36, customEnd, guiTop + 197, 2, 10, 0xFFc6c6c6);
Utils.drawDottedLine(customStart, guiTop + 36, customEnd, guiTop + 36, 2, 10, 0xFFc6c6c6);
Utils.drawDottedLine(customStart, guiTop + 197, customEnd, guiTop + 197, 2, 10, 0xFFc6c6c6);
}
if (lowestDist != null && !customSelecting) {
float price = itemData.isBz() ? itemData.bz.get(lowestDistTime).b : itemData.ah.get(lowestDistTime);
Float price2 = itemData.isBz() ? itemData.bz.get(lowestDistTime).s : null;
int xPos = (int) map(lowestDistTime, firstTime, lastTime, guiLeft + 17, guiLeft + 315);
int yPos = (int) map(price, highestValue + 10d, lowestValue - 10d, guiTop + 35, guiTop + 198);
int yPos2 = price2 != null
? (int) map(price2, highestValue + 10d, lowestValue - 10d, guiTop + 35, guiTop + 198)
: 0;
Utils.drawLine(xPos, guiTop + 35, xPos, guiTop + 198, 2, 0x4D8b8b8b);
Minecraft.getMinecraft().getTextureManager().bindTexture(TEXTURE);
GlStateManager.color(1, 1, 1, 1);
Utils.drawTexturedRect(xPos - 2.5f, yPos - 2.5f, 5, 5,
0, 5 / 512f, 247 / 512f, 252 / 512f, GL11.GL_NEAREST
);
if (price2 != null) {
Utils.drawTexturedRect(xPos - 2.5f, yPos2 - 2.5f, 5, 5,
0, 5 / 512f, 247 / 512f, 252 / 512f, GL11.GL_NEAREST
);
}
Date date = Date.from(Instant.ofEpochSecond(lowestDistTime));
SimpleDateFormat displayFormat = new SimpleDateFormat("'§b'd MMMMM yyyy '§eat§b' HH:mm");
NumberFormat nf = NumberFormat.getInstance();
ArrayList text = new ArrayList<>();
text.add(displayFormat.format(date));
if (itemData.isBz()) {
text.add(EnumChatFormatting.YELLOW + "" + EnumChatFormatting.BOLD + "Bazaar Insta-Buy: " +
EnumChatFormatting.GOLD + EnumChatFormatting.BOLD + nf.format(price));
text.add(EnumChatFormatting.YELLOW + "" + EnumChatFormatting.BOLD + "Bazaar Insta-Sell: " +
EnumChatFormatting.GOLD + EnumChatFormatting.BOLD + nf.format(price2));
} else {
text.add(EnumChatFormatting.YELLOW + "" + EnumChatFormatting.BOLD + "Lowest BIN: " +
EnumChatFormatting.GOLD + EnumChatFormatting.BOLD + nf.format(price));
}
drawHoveringText(text, xPos, yPos);
}
}
if (mouseY >= guiTop + 17 && mouseY <= guiTop + 35 && mouseX >= guiLeft + 244 && mouseX <= guiLeft + 316) {
int index = (mouseX - guiLeft - 245) / 18;
switch (index) {
case 0:
Gui.drawRect(guiLeft + 245, guiTop + 17, guiLeft + 261, guiTop + 33, 0x80ffffff);
drawHoveringText(Collections.singletonList("Show 1 Hour"), mouseX, mouseY);
break;
case 1:
Gui.drawRect(guiLeft + 263, guiTop + 17, guiLeft + 279, guiTop + 33, 0x80ffffff);
drawHoveringText(Collections.singletonList("Show 1 Day"), mouseX, mouseY);
break;
case 2:
Gui.drawRect(guiLeft + 281, guiTop + 17, guiLeft + 297, guiTop + 33, 0x80ffffff);
drawHoveringText(Collections.singletonList("Show 1 Week"), mouseX, mouseY);
break;
case 3:
Gui.drawRect(guiLeft + 299, guiTop + 17, guiLeft + 315, guiTop + 33, 0x80ffffff);
drawHoveringText(Collections.singletonList("Show All"), mouseX, mouseY);
break;
}
}
}
@Override
protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException {
super.mouseClicked(mouseX, mouseY, mouseButton);
if (mouseY >= guiTop + 17 && mouseY <= guiTop + 35 && mouseX >= guiLeft + 244 && mouseX <= guiLeft + 316) {
mode = (mouseX - guiLeft - 245) / 18;
loadData();
} else if (mouseY >= guiTop + 35 && mouseY <= guiTop + 198 && mouseX >= guiLeft + 17 && mouseX <= guiLeft + 315) {
customSelecting = true;
customStart = mouseX;
customEnd = mouseX;
}
}
@Override
protected void mouseReleased(int mouseX, int mouseY, int state) {
super.mouseReleased(mouseX, mouseY, state);
if (customSelecting) {
customSelecting = false;
customStart = (int) map(customStart, guiLeft + 17, guiLeft + 315, firstTime, lastTime);
customEnd = (int) map(mouseX, guiLeft + 17, guiLeft + 315, firstTime, lastTime);
if (customStart > customEnd) {
long temp = customStart;
customStart = customEnd;
customEnd = temp;
}
if (customEnd - customStart != 0) {
mode = 4;
loadData();
}
}
}
@Override
protected void mouseClickMove(int mouseX, int mouseY, int clickedMouseButton, long timeSinceLastClick) {
super.mouseClickMove(mouseX, mouseY, clickedMouseButton, timeSinceLastClick);
if (customSelecting) {
customEnd = mouseX < guiLeft + 18 ? guiLeft + 18 : Math.min(mouseX, guiLeft + 314);
}
}
private void loadData() {
itemData = null;
loaded = false;
new Thread(() -> {
File dir = new File("config/notenoughupdates/prices");
if (!dir.exists()) {
loaded = true;
return;
}
File[] files = dir.listFiles();
ItemData itemData = new ItemData();
if (files == null) return;
for (File file : files) {
if (!file.getName().endsWith(".gz")) continue;
HashMap data2 = load(file);
if (data2 == null || !data2.containsKey(itemId)) continue;
if (data2.get(itemId).isBz()) {
if (itemData.bz == null) itemData.bz = data2.get(itemId).bz;
else itemData.bz.putAll(data2.get(itemId).bz);
} else if (itemData.ah == null) itemData.ah = data2.get(itemId).ah;
else itemData.ah.putAll(data2.get(itemId).ah);
}
if (itemData.get() != null && !itemData.get().isEmpty()) {
if (mode < 3)
itemData = new ItemData(
new TreeMap<>(itemData.get().entrySet().stream()
.filter(e -> e.getKey() > System.currentTimeMillis() / 1000 -
(mode == 0 ? 3600 : mode == 1 ? 86400 : 604800))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))),
itemData.isBz()
);
else if (mode == 4)
itemData = new ItemData(
new TreeMap<>(itemData.get().entrySet().stream()
.filter(e -> e.getKey() >= customStart && e.getKey() <= customEnd)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))),
itemData.isBz()
);
if (itemData.get() == null || itemData.get().isEmpty()) {
loaded = true;
return;
}
this.itemData = trimData(itemData);
firstTime = this.itemData.get().firstKey();
lastTime = this.itemData.get().lastKey();
highestValue = 0;
lowestValue = null;
for (long key : this.itemData.get().keySet()) {
float value1 = this.itemData.isBz() ? this.itemData.bz.get(key).b : this.itemData.ah.get(key);
Float value2 = this.itemData.isBz() ? this.itemData.bz.get(key).s : null;
if (value1 > highestValue) {
highestValue = value1;
}
if (value2 != null && value2 > highestValue) {
highestValue = value2;
}
if (lowestValue == null || value1 < lowestValue) {
lowestValue = value1;
}
if (value2 != null && value2 < lowestValue) {
lowestValue = value2;
}
}
}
loaded = true;
}).start();
}
public static void addToCache(JsonObject items, boolean bazaar) {
if (!NotEnoughUpdates.INSTANCE.config.ahGraph.graphEnabled) return;
try {
File dir = new File("config/notenoughupdates/prices");
if (!dir.exists() && !dir.mkdir()) return;
File[] files = dir.listFiles();
if (files != null)
for (File file : files) {
if (!file.getName().endsWith(".gz")) continue;
if (file.lastModified() <
System.currentTimeMillis() - NotEnoughUpdates.INSTANCE.config.ahGraph.dataRetention * 86400000L)
//noinspection ResultOfMethodCallIgnored
file.delete();
}
Date date = new Date();
Long epochSecond = date.toInstant().getEpochSecond();
File file = new File(dir, "prices_" + format.format(date) + ".gz");
HashMap prices = new HashMap<>();
if (file.exists()) {
HashMap tempPrices = load(file);
if (tempPrices != null) prices = tempPrices;
}
for (Map.Entry item : items.entrySet()) {
addOrUpdateItemPriceInfo(item, prices, epochSecond, bazaar);
}
//noinspection ResultOfMethodCallIgnored
file.createNewFile();
try (
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
new GZIPOutputStream(new FileOutputStream(file)),
StandardCharsets.UTF_8
))
) {
writer.write(GSON.toJson(prices));
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void addOrUpdateItemPriceInfo(
Map.Entry item,
HashMap prices,
long timestamp,
boolean bazaar
) {
String itemName = item.getKey();
ItemData existingItemData = null;
if (prices.containsKey(itemName)) {
existingItemData = prices.get(itemName);
}
// Handle transitions from ah to bz (the other direction typically doesn't happen)
if (existingItemData != null) {
if (existingItemData.isBz() && !bazaar) {
return;
}
if (!existingItemData.isBz() && bazaar) {
prices.remove(itemName);
existingItemData = null;
}
}
if (bazaar) {
if (!item.getValue().getAsJsonObject().has("curr_buy") ||
!item.getValue().getAsJsonObject().has("curr_sell")
) {
return;
}
BzData bzData = new BzData(
item.getValue().getAsJsonObject().get("curr_buy").getAsFloat(),
item.getValue().getAsJsonObject().get("curr_sell").getAsFloat()
);
if (existingItemData != null) {
existingItemData.bz.put(timestamp, bzData);
} else {
TreeMap mapData = new TreeMap<>();
mapData.put(timestamp, bzData);
prices.put(item.getKey(), new ItemData(mapData, true));
}
} else {
if (existingItemData != null) {
prices.get(item.getKey()).ah.put(timestamp, item.getValue().getAsBigDecimal().intValue());
} else {
TreeMap mapData = new TreeMap<>();
mapData.put(timestamp, item.getValue().getAsLong());
prices.put(item.getKey(), new ItemData(mapData, false));
}
}
}
private ItemData trimData(ItemData itemData) {
long first = itemData.get().firstKey();
long last = itemData.get().lastKey();
ItemData trimmed = new ItemData();
if (itemData.isBz())
trimmed.bz = new TreeMap<>();
else
trimmed.ah = new TreeMap<>();
int zones = NotEnoughUpdates.INSTANCE.config.ahGraph.graphZones;
Long[] dataArray =
!itemData.isBz() ? itemData.ah.keySet().toArray(new Long[0]) : itemData.bz.keySet().toArray(new Long[0]);
int prev = 0;
for (int i = 0; i < zones; i++) {
long lowest = (long) map(i, 0, zones, first, last);
long highest = (long) map(i + 1, 0, zones, first, last);
int amount = 0;
double sumBuy = 0;
double sumSell = 0;
for (int l = prev; l < dataArray.length; l++) {
if (dataArray[l] >= lowest && dataArray[l] <= highest) {
amount++;
sumBuy += itemData.isBz() ? itemData.bz.get(dataArray[l]).b : itemData.ah.get(dataArray[l]);
if (itemData.isBz()) sumSell += itemData.bz.get(dataArray[l]).s;
prev = l + 1;
} else if (dataArray[l] > highest)
break;
}
if (amount > 0) {
if (itemData.isBz())
trimmed.bz.put(lowest, new BzData((float) (sumBuy / amount), (float) (sumSell / amount)));
else
trimmed.ah.put(lowest, (int) (sumBuy / amount));
}
}
return trimmed;
}
private static HashMap load(File file) {
Type type = new TypeToken>() {
}.getType();
if (file.exists()) {
try (
BufferedReader reader = new BufferedReader(new InputStreamReader(
new GZIPInputStream(new FileInputStream(file)),
StandardCharsets.UTF_8
))
) {
return GSON.fromJson(reader, type);
} catch (Exception e) {
System.out.println("Deleting " + file.getName() + " because it is probably corrupted.");
//noinspection ResultOfMethodCallIgnored
file.delete();
}
}
return null;
}
private static double map(double x, double in_min, double in_max, double out_min, double out_max) {
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
private static String formatPrice(long price) {
DecimalFormat df = new DecimalFormat("#.00");
if (price >= 1000000000) {
return df.format(price / 1000000000f) + "B";
} else if (price >= 1000000) {
return df.format(price / 1000000f) + "M";
} else if (price >= 1000) {
return df.format(price / 1000f) + "K";
}
return String.valueOf(price);
}
private int changeAlpha(int origColor, int alpha) {
origColor = origColor & 0x00ffffff; //drop the previous alpha value
return (alpha << 24) | origColor; //add the one the user inputted
}
}
class ItemData {
public TreeMap ah = null;
public TreeMap bz = null;
public ItemData() {
}
public ItemData(TreeMap map, boolean bz) {
if (bz)
this.bz = (TreeMap) map;
else
this.ah = (TreeMap) map;
}
public TreeMap get() {
return !isBz() ? ah : bz;
}
public boolean isBz() {
return bz != null && !bz.isEmpty();
}
}
class BzData {
float b;
float s;
public BzData(float b, float s) {
this.b = b;
this.s = s;
}
}