/*
* Copyright (C) 2022-2023 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.miscfeatures;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import io.github.moulberry.notenoughupdates.NotEnoughUpdates;
import io.github.moulberry.notenoughupdates.core.config.ConfigUtil;
import io.github.moulberry.notenoughupdates.miscgui.StorageOverlay;
import io.github.moulberry.notenoughupdates.util.SBInfo;
import io.github.moulberry.notenoughupdates.util.Utils;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.inventory.GuiChest;
import net.minecraft.init.Blocks;
import net.minecraft.init.Items;
import net.minecraft.inventory.ContainerChest;
import net.minecraft.inventory.IInventory;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.JsonToNBT;
import net.minecraft.nbt.NBTBase;
import net.minecraft.nbt.NBTException;
import net.minecraft.nbt.NBTTagByte;
import net.minecraft.nbt.NBTTagByteArray;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagDouble;
import net.minecraft.nbt.NBTTagFloat;
import net.minecraft.nbt.NBTTagInt;
import net.minecraft.nbt.NBTTagIntArray;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.nbt.NBTTagLong;
import net.minecraft.nbt.NBTTagShort;
import net.minecraft.nbt.NBTTagString;
import net.minecraft.network.play.client.C0EPacketClickWindow;
import net.minecraft.network.play.server.S2DPacketOpenWindow;
import net.minecraft.network.play.server.S2EPacketCloseWindow;
import net.minecraft.network.play.server.S2FPacketSetSlot;
import net.minecraft.network.play.server.S30PacketWindowItems;
import java.io.File;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class StorageManager {
private static final StorageManager INSTANCE = new StorageManager();
private static final Gson GSON = new GsonBuilder()
.registerTypeAdapter(ItemStack.class, new ItemStackSerializer())
.registerTypeAdapter(ItemStack.class, new ItemStackDeserializer())
.create();
public static class ItemStackSerializer implements JsonSerializer {
@Override
public JsonElement serialize(ItemStack src, Type typeOfSrc, JsonSerializationContext context) {
fixPetInfo(src);
NBTTagCompound tag = src.serializeNBT();
return nbtToJson(tag);
}
}
private static final Pattern JSON_FIX_REGEX = Pattern.compile("\"([^,:]+)\":");
public static class ItemStackDeserializer implements JsonDeserializer {
@Override
public ItemStack deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
try {
JsonObject object = json.getAsJsonObject();
NBTTagCompound tag = JsonToNBT.getTagFromJson(JSON_FIX_REGEX.matcher(object.toString()).replaceAll("$1:"));
Item item;
if (tag.hasKey("id", 8)) {
item = Item.getByNameOrId(tag.getString("id"));
} else {
item = Item.getItemById(tag.getShort("id"));
}
if (item == null) {
return null;
}
int stackSize = tag.getInteger("Count");
int damage = tag.getInteger("Damage");
ItemStack stack = new ItemStack(item, stackSize, damage);
if (tag.hasKey("tag")) {
NBTTagCompound itemTag = tag.getCompoundTag("tag");
stack.setTagCompound(itemTag);
}
return stack;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
private static JsonObject nbtToJson(NBTTagCompound NBTTagCompound) {
return (JsonObject) loadJson(NBTTagCompound);
}
private static class PetInfo {
String type;
Boolean active;
Double exp;
String tier;
Boolean hideInfo;
Integer candyUsed;
String uuid;
Boolean hideRightClick;
String heldItem;
String skin;
private void appendIfNotNull(StringBuilder builder, String key, T value) {
if (value != null) {
if (builder.indexOf("{") != builder.length() - 1) {
builder.append(",");
}
builder.append(key).append(":");
if (value instanceof String) {
builder.append("\"").append(value).append("\"");
} else {
builder.append(value);
}
}
}
@Override
public String toString() {
StringBuilder object = new StringBuilder();
object.append("{");
appendIfNotNull(object, "type", type);
appendIfNotNull(object, "active", active);
appendIfNotNull(object, "exp", exp);
appendIfNotNull(object, "tier", tier);
appendIfNotNull(object, "hideInfo", hideInfo);
appendIfNotNull(object, "candyUsed", candyUsed);
appendIfNotNull(object, "uuid", uuid);
appendIfNotNull(object, "hideRightClick", hideRightClick);
appendIfNotNull(object, "heldItem", heldItem);
appendIfNotNull(object, "skin", skin);
object.append("}");
return object.toString();
}
}
private static void fixPetInfo(ItemStack src) {
if (src.getTagCompound() == null || !src.getTagCompound().hasKey("ExtraAttributes") ||
!src.getTagCompound().getCompoundTag("ExtraAttributes").hasKey("petInfo")) return;
PetInfo oldPetInfo = GSON.fromJson(
src.getTagCompound().getCompoundTag("ExtraAttributes").getString("petInfo"),
PetInfo.class
);
src.getTagCompound().getCompoundTag("ExtraAttributes").removeTag("petInfo");
try {
src.getTagCompound().getCompoundTag("ExtraAttributes").setTag(
"petInfo",
JsonToNBT.getTagFromJson(oldPetInfo.toString())
);
} catch (NBTException | NullPointerException ignored) {
}
}
private static JsonElement loadJson(NBTBase tag) {
if (tag instanceof NBTTagCompound) {
NBTTagCompound compoundTag = (NBTTagCompound) tag;
JsonObject jsonObject = new JsonObject();
for (String key : compoundTag.getKeySet()) {
jsonObject.add(key, loadJson(compoundTag.getTag(key)));
}
return jsonObject;
} else if (tag instanceof NBTTagList) {
NBTTagList listTag = (NBTTagList) tag;
JsonArray jsonArray = new JsonArray();
for (int i = 0; i < listTag.tagCount(); i++) {
jsonArray.add(loadJson(listTag.get(i)));
}
return jsonArray;
} else if (tag instanceof NBTTagIntArray) {
NBTTagIntArray listTag = (NBTTagIntArray) tag;
int[] arr = listTag.getIntArray();
JsonArray jsonArray = new JsonArray();
for (int j : arr) {
jsonArray.add(new JsonPrimitive(j));
}
return jsonArray;
} else if (tag instanceof NBTTagByteArray) {
NBTTagByteArray listTag = (NBTTagByteArray) tag;
byte[] arr = listTag.getByteArray();
JsonArray jsonArray = new JsonArray();
for (byte b : arr) {
jsonArray.add(new JsonPrimitive(b));
}
return jsonArray;
} else if (tag instanceof NBTTagShort) {
return new JsonPrimitive(((NBTTagShort) tag).getShort());
} else if (tag instanceof NBTTagInt) {
return new JsonPrimitive(((NBTTagInt) tag).getInt());
} else if (tag instanceof NBTTagLong) {
return new JsonPrimitive(((NBTTagLong) tag).getLong());
} else if (tag instanceof NBTTagFloat) {
return new JsonPrimitive(((NBTTagFloat) tag).getFloat());
} else if (tag instanceof NBTTagDouble) {
return new JsonPrimitive(((NBTTagDouble) tag).getDouble());
} else if (tag instanceof NBTTagByte) {
return new JsonPrimitive(((NBTTagByte) tag).getByte());
} else if (tag instanceof NBTTagString) {
return new JsonPrimitive(((NBTTagString) tag).getString());
} else {
return new JsonPrimitive("Broken_Json_Deserialize_Tag");
}
}
public static StorageManager getInstance() {
return INSTANCE;
}
private final AtomicInteger searchId = new AtomicInteger(0);
public static class StoragePage {
public ItemStack[] items = new ItemStack[45];
public ItemStack backpackDisplayStack;
public String customTitle;
public int rows = -1;
public boolean[] shouldDarkenIfNotSelected = new boolean[45];
public transient boolean matchesSearch;
public transient int searchedId;
}
public static int MAX_ENDER_CHEST_PAGES = 9;
public static final ItemStack LOCKED_ENDERCHEST_STACK =
Utils.createItemStack(
Item.getItemFromBlock(Blocks.stained_glass_pane),
"\u00a7cLocked Page",
14,
"\u00a77Unlock more Ender Chest",
"\u00a77pages in the community",
"\u00a77shop!"
);
public static class StorageConfig {
public HashMap pages = new HashMap<>();
public final HashMap displayToStorageIdMap = new HashMap<>();
public final HashMap displayToStorageIdMapRender = new HashMap<>();
}
public StorageConfig storageConfig = new StorageConfig();
private int currentStoragePage = -1;
public boolean onStorageMenu = false;
private String lastSearch = "";
private boolean[] storagePresent = null;
public long storageOpenSwitchMillis = 0;
private final ItemStack[] missingBackpackStacks = new ItemStack[18];
private boolean shouldRenderStorageOverlayCached = false;
private static final Pattern WINDOW_REGEX = Pattern.compile(".+ Backpack (?:✦ )?\\(Slot #(\\d+)\\)");
private static final Pattern ECHEST_WINDOW_REGEX = Pattern.compile("Ender Chest \\((\\d+)/(\\d+)\\)");
public void loadConfig(File file) {
storageConfig = ConfigUtil.loadConfig(StorageConfig.class, file, GSON, true);
if (storageConfig == null) {
storageConfig = new StorageConfig();
}
}
public void saveConfig(File file) {
ConfigUtil.saveConfig(storageConfig, file, GSON, true);
}
public ItemStack getMissingBackpackStack(int storageId) {
if (missingBackpackStacks[storageId] != null) {
return missingBackpackStacks[storageId];
}
ItemStack stack = Utils.createItemStack(
Item.getItemFromBlock(Blocks.stained_glass_pane),
"\u00a7eEmpty Backpack Slot " + (storageId + 1),
12,
storageId + 1,
"",
"\u00a7eLeft-click a backpack",
"\u00a7eitem on this slot to place",
"\u00a7eit!"
);
missingBackpackStacks[storageId] = stack;
return stack;
}
public boolean isStorageOpen = false;
public boolean shouldRenderStorageOverlay(String containerName) {
isStorageOpen = false;
if (!NotEnoughUpdates.INSTANCE.config.storageGUI.enableStorageGUI3) {
shouldRenderStorageOverlayCached = false;
return false;
}
if (!NotEnoughUpdates.INSTANCE.hasSkyblockScoreboard()) {
shouldRenderStorageOverlayCached = false;
return false;
}
if (!(Minecraft.getMinecraft().currentScreen instanceof GuiChest)) {
shouldRenderStorageOverlayCached = false;
return false;
}
if (getCurrentWindowId() != -1 && getCurrentPageId() != -1) {
shouldRenderStorageOverlayCached = true;
isStorageOpen = true;
return true;
}
shouldRenderStorageOverlayCached = containerName != null && containerName.trim().equals("Storage");
isStorageOpen = shouldRenderStorageOverlayCached;
return shouldRenderStorageOverlayCached;
}
public boolean shouldRenderStorageOverlayFast() {
return shouldRenderStorageOverlayCached;
}
private StoragePage[] getPagesForProfile() {
if (!NotEnoughUpdates.INSTANCE.hasSkyblockScoreboard()) return null;
if (SBInfo.getInstance().currentProfile == null) return null;
return storageConfig.pages.computeIfAbsent(SBInfo.getInstance().currentProfile, k -> new StoragePage[27]);
}
public StoragePage getPage(int pageIndex, boolean createPage) {
if (pageIndex == -1) return null;
StoragePage[] pages = getPagesForProfile();
if (pages == null) return null;
if (createPage && pages[pageIndex] == null) pages[pageIndex] = new StoragePage();
return pages[pageIndex];
}
public void removePage(int pageIndex) {
if (pageIndex == -1) return;
StoragePage[] pages = getPagesForProfile();
if (pages == null) return;
pages[pageIndex] = null;
}
public StoragePage getCurrentPage() {
return getPage(getCurrentPageId(), true);
}
private void setItemSlot(int index, ItemStack item) {
StoragePage page = getCurrentPage();
if (page != null) {
page.items[index] = item;
}
}
public int getCurrentPageId() {
if (!(Minecraft.getMinecraft().currentScreen instanceof GuiChest)) {
currentStoragePage = -1;
return -1;
}
return currentStoragePage;
}
public int getCurrentWindowId() {
if (!(Minecraft.getMinecraft().currentScreen instanceof GuiChest)) {
currentStoragePage = -1;
return -1;
}
GuiChest chest = (GuiChest) Minecraft.getMinecraft().currentScreen;
return chest.inventorySlots.windowId;
}
public void sendToPage(int page) {
if (System.currentTimeMillis() - storageOpenSwitchMillis < 100) return;
if (getCurrentPageId() == page) return;
if (page < MAX_ENDER_CHEST_PAGES) {
NotEnoughUpdates.INSTANCE.sendChatMessage("/enderchest " + (page + 1));
} else {
NotEnoughUpdates.INSTANCE.sendChatMessage("/backpack " + (page + 1 - MAX_ENDER_CHEST_PAGES));
}
storageOpenSwitchMillis = System.currentTimeMillis();
}
public int getDisplayIdForStorageId(int storageId) {
if (storageId < 0) return -1;
for (Map.Entry entry : storageConfig.displayToStorageIdMap.entrySet()) {
if (entry.getValue() == storageId) {
return entry.getKey();
}
}
return -1;
}
public int getDisplayIdForStorageIdRender(int storageId) {
if (storageId < 0) return -1;
for (Map.Entry entry : storageConfig.displayToStorageIdMapRender.entrySet()) {
if (entry.getValue() == storageId) {
return entry.getKey();
}
}
return -1;
}
public void openWindowPacket(S2DPacketOpenWindow packet) {
shouldRenderStorageOverlayCached = false;
if (!NotEnoughUpdates.INSTANCE.hasSkyblockScoreboard()) return;
String windowTitle = Utils.cleanColour(packet.getWindowTitle().getUnformattedText());
Matcher matcher = WINDOW_REGEX.matcher(windowTitle);
Matcher matcherEchest = ECHEST_WINDOW_REGEX.matcher(windowTitle);
currentStoragePage = -1;
onStorageMenu = false;
if (windowTitle.trim().equals("Storage")) {
onStorageMenu = true;
} else if (matcher.matches()) {
int page = Integer.parseInt(matcher.group(1));
if (page > 0 && page <= 18) {
currentStoragePage = page - 1 + MAX_ENDER_CHEST_PAGES;
int displayId = getDisplayIdForStorageId(currentStoragePage);
if (displayId >= 0) StorageOverlay.getInstance().scrollToStorage(displayId, false);
StoragePage spage = getCurrentPage();
if (spage != null) {
spage.rows = packet.getSlotCount() / 9 - 1;
}
}
} else if (matcherEchest.matches()) {
int page = Integer.parseInt(matcherEchest.group(1));
if (page > 0 && page <= 9) {
currentStoragePage = page - 1;
int displayId = getDisplayIdForStorageId(currentStoragePage);
if (displayId >= 0) StorageOverlay.getInstance().scrollToStorage(displayId, false);
StoragePage spage = getCurrentPage();
if (spage != null) {
spage.rows = packet.getSlotCount() / 9 - 1;
}
}
} else {
StorageOverlay.getInstance().clearSearch();
return;
}
StorageOverlay.getInstance().fastRenderCheck();
}
public void closeWindowPacket(S2EPacketCloseWindow packet) {
shouldRenderStorageOverlayCached = false;
}
public void setSlotPacket(S2FPacketSetSlot packet) {
if (!NotEnoughUpdates.INSTANCE.hasSkyblockScoreboard()) return;
if (getCurrentWindowId() == -1 || getCurrentWindowId() != packet.func_149175_c()) return;
if (getCurrentPageId() != -1) {
StoragePage page = getCurrentPage();
int slot = packet.func_149173_d();
if (page != null && slot >= 9 && slot < 9 + page.rows * 9) {
setItemSlot(packet.func_149173_d() - 9, packet.func_149174_e());
}
} else if (onStorageMenu) {
if (storagePresent == null) {
storagePresent = new boolean[27];
}
int slot = packet.func_149173_d();
ItemStack stack = packet.func_149174_e();
if (slot >= 9 && slot < 18) {
int index = slot - 9;
boolean changed = false;
if ((stack.getItem() == Item.getItemFromBlock(Blocks.stained_glass_pane) &&
stack.getMetadata() == 14) || (stack.getItem() == Items.dye && stack.getMetadata() == 8)) {
if (storagePresent[index]) changed = true;
storagePresent[index] = false;
removePage(index);
} else {
if (!storagePresent[index]) changed = true;
storagePresent[index] = true;
getPage(index, true).backpackDisplayStack = stack;
}
if (changed) {
synchronized (storageConfig.displayToStorageIdMap) {
storageConfig.displayToStorageIdMap.clear();
storageConfig.displayToStorageIdMapRender.clear();
int displayIndex = 0;
for (int i = 0; i < storagePresent.length; i++) {
if (storagePresent[i]) {
storageConfig.displayToStorageIdMap.put(displayIndex, i);
if (lastSearch != null && !lastSearch.isEmpty()) {
StoragePage page = getPage(i, false);
if (page != null) {
updateSearchForPage(lastSearch, page);
if (page.matchesSearch) {
storageConfig.displayToStorageIdMapRender.put(displayIndex++, i);
}
}
} else
storageConfig.displayToStorageIdMapRender.put(displayIndex++, i);
}
}
}
}
}
if (slot >= 27 && slot < 45) {
int index = (slot - 27) % 9 + (slot - 27) / 9 * 9 + MAX_ENDER_CHEST_PAGES;
boolean changed = false;
if (stack.getItem() == Item.getItemFromBlock(Blocks.stained_glass_pane)
|| (stack.getItem() == Items.dye && stack.getMetadata() == 8)) {
if (storagePresent[index]) changed = true;
storagePresent[index] = false;
removePage(index);
} else {
if (!storagePresent[index]) changed = true;
storagePresent[index] = true;
getPage(index, true).backpackDisplayStack = stack;
}
if (changed) {
synchronized (storageConfig.displayToStorageIdMap) {
storageConfig.displayToStorageIdMap.clear();
storageConfig.displayToStorageIdMapRender.clear();
int displayIndex = 0;
for (int i = 0; i < storagePresent.length; i++) {
if (storagePresent[i]) {
storageConfig.displayToStorageIdMap.put(displayIndex, i);
if (lastSearch != null && !lastSearch.isEmpty()) {
StoragePage page = getPage(i, false);
if (page != null) {
updateSearchForPage(lastSearch, page);
if (page.matchesSearch) {
storageConfig.displayToStorageIdMapRender.put(displayIndex++, i);
}
}
} else
storageConfig.displayToStorageIdMapRender.put(displayIndex++, i);
}
}
}
}
}
}
}
public void updateSearchForPage(String searchStr, StoragePage page) {
if (page == null) {
return;
}
if (page.rows <= 0) {
page.matchesSearch = true;
return;
}
if (page.searchedId > searchId.get()) {
page.searchedId = -1;
return;
}
if (page.searchedId == searchId.get()) {
return;
}
page.searchedId = searchId.get();
if (searchStr == null || searchStr.trim().isEmpty()) {
page.matchesSearch = true;
return;
}
for (ItemStack stack : page.items) {
if (stack != null && NotEnoughUpdates.INSTANCE.manager.doesStackMatchSearch(stack, searchStr)) {
page.matchesSearch = true;
return;
}
}
page.matchesSearch = false;
}
public void searchDisplay(String searchStr) {
if (storagePresent == null) return;
synchronized (storageConfig.displayToStorageIdMapRender) {
storageConfig.displayToStorageIdMapRender.clear();
lastSearch = searchStr;
int sid = searchId.incrementAndGet();
int displayIndex = 0;
for (int i = 0; i < storagePresent.length; i++) {
if (storagePresent[i]) {
StoragePage page = getPage(i, false);
if (page != null) {
if (page.rows > 0) {
updateSearchForPage(searchStr, page);
if (page.matchesSearch) {
storageConfig.displayToStorageIdMapRender.put(displayIndex++, i);
}
} else {
storageConfig.displayToStorageIdMapRender.put(displayIndex++, i);
page.matchesSearch = true;
page.searchedId = sid;
}
}
}
}
}
}
public void setItemsPacket(S30PacketWindowItems packet) {
if (!NotEnoughUpdates.INSTANCE.hasSkyblockScoreboard()) return;
if (getCurrentWindowId() == -1 || getCurrentWindowId() != packet.func_148911_c()) return;
if (getCurrentPageId() != -1) {
StoragePage page = getPage(getCurrentPageId(), false);
if (page != null) {
int max = Math.min(page.rows * 9, packet.getItemStacks().length - 9);
for (int i = 0; i < max; i++) {
setItemSlot(i, packet.getItemStacks()[i + 9]);
}
}
}
}
public void clientSendWindowClick(C0EPacketClickWindow packet) {
if (!NotEnoughUpdates.INSTANCE.hasSkyblockScoreboard()) return;
if (getCurrentWindowId() == -1 || getCurrentWindowId() != packet.getWindowId()) return;
if (!(Minecraft.getMinecraft().currentScreen instanceof GuiChest)) return;
ContainerChest containerChest = (ContainerChest) ((GuiChest) Minecraft.getMinecraft().currentScreen).inventorySlots;
if (getCurrentPageId() != -1) {
StoragePage page = getCurrentPage();
if (page == null) return;
IInventory inv = containerChest.getLowerChestInventory();
int max = Math.min(9 + page.rows * 9, inv.getSizeInventory());
for (int i = 9; i < max; i++) {
setItemSlot(i - 9, inv.getStackInSlot(i));
}
}
}
}