summaryrefslogtreecommitdiff
path: root/src/main
diff options
context:
space:
mode:
authorunknown <james.jenour@protonmail.com>2020-03-29 15:32:45 +1100
committerunknown <james.jenour@protonmail.com>2020-03-29 15:32:45 +1100
commitae5bace43a14f2d8be9f1c1d9c644cc5dabd5d2f (patch)
tree86246c8474241da955e9bd0bab93dd35f4a73b88 /src/main
downloadNotEnoughUpdates-ae5bace43a14f2d8be9f1c1d9c644cc5dabd5d2f.tar.gz
NotEnoughUpdates-ae5bace43a14f2d8be9f1c1d9c644cc5dabd5d2f.tar.bz2
NotEnoughUpdates-ae5bace43a14f2d8be9f1c1d9c644cc5dabd5d2f.zip
Initial (1.0-BETA)
Diffstat (limited to 'src/main')
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/LerpingFloat.java68
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/LerpingInteger.java68
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/NEUIO.java98
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/NEUManager.java627
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/NEUOverlay.java804
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/NotEnoughUpdates.java159
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/itemeditor/GuiElement.java15
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/itemeditor/GuiElementButton.java35
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/itemeditor/GuiElementText.java42
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/itemeditor/GuiElementTextField.java407
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/itemeditor/NEUItemEditor.java417
-rw-r--r--src/main/resources/assets/notenoughupdates/item_edit.pngbin0 -> 11387 bytes
-rw-r--r--src/main/resources/assets/notenoughupdates/item_pane_tab_arrow.pngbin0 -> 7532 bytes
-rw-r--r--src/main/resources/assets/notenoughupdates/next.pngbin0 -> 5005 bytes
-rw-r--r--src/main/resources/assets/notenoughupdates/next_pow2.pngbin0 -> 17733 bytes
-rw-r--r--src/main/resources/assets/notenoughupdates/prev.pngbin0 -> 5207 bytes
-rw-r--r--src/main/resources/assets/notenoughupdates/prev_pow2.pngbin0 -> 18696 bytes
-rw-r--r--src/main/resources/mcmod.info16
18 files changed, 2756 insertions, 0 deletions
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/LerpingFloat.java b/src/main/java/io/github/moulberry/notenoughupdates/LerpingFloat.java
new file mode 100644
index 00000000..e2cca0c0
--- /dev/null
+++ b/src/main/java/io/github/moulberry/notenoughupdates/LerpingFloat.java
@@ -0,0 +1,68 @@
+package io.github.moulberry.notenoughupdates;
+
+public class LerpingFloat {
+
+ private int timeSpent;
+ private long lastMillis;
+ private int timeToReachTarget;
+
+ private float targetValue;
+ private float lerpValue;
+
+ public LerpingFloat(float initialValue, int timeToReachTarget) {
+ this.targetValue = this.lerpValue = initialValue;
+ this.timeToReachTarget = timeToReachTarget;
+ }
+
+ public LerpingFloat(int initialValue) {
+ this(initialValue, 200);
+ }
+
+ public void tick() {
+ int lastTimeSpent = timeSpent;
+ this.timeSpent += System.currentTimeMillis() - lastMillis;
+
+ float lastDistPercentToTarget = lastTimeSpent/(float)timeToReachTarget;
+ float distPercentToTarget = timeSpent/(float)timeToReachTarget;
+ float fac = (1-lastDistPercentToTarget)/lastDistPercentToTarget;
+
+ float startValue = lerpValue - (targetValue - lerpValue)/fac;
+
+ float dist = targetValue - startValue;
+ if(dist == 0) return;
+
+ float oldLerpValue = lerpValue;
+ if(distPercentToTarget >= 1) {
+ lerpValue = targetValue;
+ } else {
+ lerpValue = startValue + dist*distPercentToTarget;
+ }
+
+ if(lerpValue == oldLerpValue) {
+ timeSpent = lastTimeSpent;
+ } else {
+ this.lastMillis = System.currentTimeMillis();
+ }
+ }
+
+ public void resetTimer() {
+ this.timeSpent = 0;
+ this.lastMillis = System.currentTimeMillis();
+ }
+
+ public void setTarget(float targetValue) {
+ this.targetValue = targetValue;
+ }
+
+ public void setValue(float value) {
+ this.targetValue = this.lerpValue = value;
+ }
+
+ public float getValue() {
+ return lerpValue;
+ }
+
+ public float getTarget() {
+ return targetValue;
+ }
+}
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/LerpingInteger.java b/src/main/java/io/github/moulberry/notenoughupdates/LerpingInteger.java
new file mode 100644
index 00000000..2f21927a
--- /dev/null
+++ b/src/main/java/io/github/moulberry/notenoughupdates/LerpingInteger.java
@@ -0,0 +1,68 @@
+package io.github.moulberry.notenoughupdates;
+
+public class LerpingInteger {
+
+ private int timeSpent;
+ private long lastMillis;
+ private int timeToReachTarget;
+
+ private int targetValue;
+ private int lerpValue;
+
+ public LerpingInteger(int initialValue, int timeToReachTarget) {
+ this.targetValue = this.lerpValue = initialValue;
+ this.timeToReachTarget = timeToReachTarget;
+ }
+
+ public LerpingInteger(int initialValue) {
+ this(initialValue, 200);
+ }
+
+ public void tick() {
+ int lastTimeSpent = timeSpent;
+ this.timeSpent += System.currentTimeMillis() - lastMillis;
+
+ float lastDistPercentToTarget = lastTimeSpent/(float)timeToReachTarget;
+ float distPercentToTarget = timeSpent/(float)timeToReachTarget;
+ float fac = (1-lastDistPercentToTarget)/lastDistPercentToTarget;
+
+ int startValue = lerpValue - (int)((targetValue - lerpValue)/fac);
+
+ int dist = targetValue - startValue;
+ if(dist == 0) return;
+
+ int oldLerpValue = lerpValue;
+ if(distPercentToTarget >= 1) {
+ lerpValue = targetValue;
+ } else {
+ lerpValue = startValue + (int)(dist*distPercentToTarget);
+ }
+
+ if(lerpValue == oldLerpValue) {
+ timeSpent = lastTimeSpent;
+ } else {
+ this.lastMillis = System.currentTimeMillis();
+ }
+ }
+
+ public void resetTimer() {
+ this.timeSpent = 0;
+ this.lastMillis = System.currentTimeMillis();
+ }
+
+ public void setTarget(int targetValue) {
+ this.targetValue = targetValue;
+ }
+
+ public void setValue(int value) {
+ this.targetValue = this.lerpValue = value;
+ }
+
+ public int getValue() {
+ return lerpValue;
+ }
+
+ public int getTarget() {
+ return targetValue;
+ }
+}
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/NEUIO.java b/src/main/java/io/github/moulberry/notenoughupdates/NEUIO.java
new file mode 100644
index 00000000..7e7091cf
--- /dev/null
+++ b/src/main/java/io/github/moulberry/notenoughupdates/NEUIO.java
@@ -0,0 +1,98 @@
+package io.github.moulberry.notenoughupdates;
+
+import org.kohsuke.github.*;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class NEUIO {
+
+ private final String accessToken;
+
+ public NEUIO(String accessToken) {
+ this.accessToken = accessToken;
+ }
+
+ /**
+ * Creates a new branch, commits to it with a single file change and submits a pull request from the new branch
+ * back to the master branch.
+ */
+ public boolean createNewRequest(String newBranchName, String prTitle, String prBody, String filename, String content) {
+ try {
+ GitHub github = new GitHubBuilder().withOAuthToken(accessToken).build();
+ System.out.println("Getting repo");
+
+ //https://github.com/Moulberry/NotEnoughUpdates-REPO
+ GHRepository repo = github.getRepositoryById("247692460");
+
+ System.out.println("Getting last commit");
+ String lastCommitSha = repo.getRef("heads/master").getObject().getSha();
+ System.out.println("Last master commit sha: " + lastCommitSha);
+
+ String lastTreeSha = repo.getCommit(lastCommitSha).getTree().getSha();
+
+ GHTreeBuilder tb = repo.createTree();
+ tb.baseTree(lastTreeSha);
+ tb.add(filename, content, false);
+ GHTree tree = tb.create();
+ System.out.println("Created new tree: " + tree.getSha());
+
+ GHCommitBuilder cb = repo.createCommit();
+ cb.message(prTitle);
+ cb.tree(tree.getSha());
+ cb.parent(lastCommitSha);
+ GHCommit commit = cb.create();
+ System.out.println("Created commit: " + commit.getSHA1());
+
+ repo.createRef("refs/heads/"+newBranchName, commit.getSHA1());
+ System.out.println("Set new branch head to commit.");
+
+ repo.createPullRequest(prTitle, newBranchName, "master", prBody);
+ return true;
+ } catch(IOException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ /**
+ * @param oldShas Map from filename (eg. BOW.json) to the sha in the local repository
+ * @return Map from filename to the new shas
+ */
+ public Map<String, String> getChangedItems(Map<String, String> oldShas) {
+ HashMap<String, String> changedFiles = new HashMap<>();
+ try {
+ GitHub github = new GitHubBuilder().withOAuthToken(accessToken).build();
+ GHRepository repo = github.getRepositoryById("247692460");
+
+ for(GHContent content : repo.getDirectoryContent("items")) {
+ String oldSha = oldShas.get(content.getName());
+ if(!content.getSha().equals(oldSha)) {
+ changedFiles.put(content.getName(), content.getSha());
+ }
+ }
+ } catch(IOException e) { }
+ return changedFiles;
+ }
+
+ /**
+ * Takes set of filename (eg. BOW.json) and returns map from that filename to the individual download link.
+ */
+ public Map<String, String> getItemsDownload(Set<String> filename) {
+ HashMap<String, String> downloadUrls = new HashMap<>();
+ try {
+ GitHub github = new GitHubBuilder().withOAuthToken(accessToken).build();
+ GHRepository repo = github.getRepositoryById("247692460");
+
+ for(GHContent content : repo.getDirectoryContent("items")) {
+ if(filename.contains(content.getName())) {
+ downloadUrls.put(content.getName(), content.getDownloadUrl());
+ }
+ }
+ } catch(IOException e) { }
+ return downloadUrls;
+ }
+}
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/NEUManager.java b/src/main/java/io/github/moulberry/notenoughupdates/NEUManager.java
new file mode 100644
index 00000000..c6fab0f1
--- /dev/null
+++ b/src/main/java/io/github/moulberry/notenoughupdates/NEUManager.java
@@ -0,0 +1,627 @@
+package io.github.moulberry.notenoughupdates;
+
+import com.google.gson.*;
+import javafx.scene.control.Alert;
+import net.minecraft.client.Minecraft;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.JsonToNBT;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.nbt.NBTTagList;
+import net.minecraft.nbt.NBTUtil;
+import net.minecraft.util.ResourceLocation;
+import org.lwjgl.opengl.Display;
+
+import javax.swing.*;
+import java.io.*;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+public class NEUManager {
+
+ public final NEUIO neuio;
+ public static final Gson gson = new GsonBuilder().setPrettyPrinting().create();
+
+ private TreeMap<String, JsonObject> itemMap = new TreeMap<>();
+
+ private TreeMap<String, Set<String>> tagWordMap = new TreeMap<>();
+ private TreeMap<String, HashMap<String, List<Integer>>> titleWordMap = new TreeMap<>();
+ private TreeMap<String, HashMap<String, List<Integer>>> loreWordMap = new TreeMap<>();
+
+ private File configLocation;
+ private File itemsLocation;
+ private File itemShaLocation;
+ private JsonObject itemShaConfig;
+ private File config;
+ private JsonObject configJson;
+
+ public NEUManager(NEUIO neuio, File configLocation) {
+ this.configLocation = configLocation;
+ this.neuio = neuio;
+
+ this.config = new File(configLocation, "config.json");
+ try {
+ config.createNewFile();
+ configJson = getJsonFromFile(config);
+ if(configJson == null) configJson = new JsonObject();
+ } catch(IOException e) { }
+
+ this.itemsLocation = new File(configLocation, "items");
+ itemsLocation.mkdir();
+
+ this.itemShaLocation = new File(configLocation, "itemSha.json");
+ try {
+ itemShaLocation.createNewFile();
+ itemShaConfig = getJsonFromFile(itemShaLocation);
+ if(itemShaConfig == null) itemShaConfig = new JsonObject();
+ } catch(IOException e) { }
+ }
+
+ /**
+ * Parses a file in to a JsonObject.
+ */
+ public JsonObject getJsonFromFile(File file) throws IOException {
+ InputStream in = new FileInputStream(file);
+ BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
+ JsonObject json = gson.fromJson(reader, JsonObject.class);
+ return json;
+ }
+
+ /**
+ * Some convenience methods for working with the config. Should probably switch to using a more sophisticated
+ * config system, but until we only use it for more than one value, this should be fine.
+ */
+ public void setAllowEditing(boolean allowEditing) {
+ try {
+ configJson.addProperty("allowEditing", allowEditing);
+ writeJson(configJson, config);
+ } catch(IOException e) {}
+ }
+
+ public boolean getAllowEditing() {
+ try {
+ return configJson.get("allowEditing").getAsBoolean();
+ } catch(Exception e) {}
+ return false;
+ }
+
+ /**
+ * Called when the game is first loaded. Compares the local repository to the github repository and handles
+ * the downloading of new/updated files. This then calls the "loadItem" method for every item in the local
+ * repository.
+ */
+ public void loadItemInformation() {
+ JOptionPane pane = new JOptionPane("Getting items to download from remote repository.");
+ JDialog dialog = pane.createDialog("NotEnoughUpdates Remote Sync");
+ dialog.setModal(false);
+ dialog.setVisible(true);
+
+ if(Display.isActive()) dialog.toFront();
+
+ HashMap<String, String> oldShas = new HashMap<>();
+ for(Map.Entry<String, JsonElement> entry : itemShaConfig.entrySet()) {
+ if(new File(itemsLocation, entry.getKey()+".json").exists()) {
+ oldShas.put(entry.getKey()+".json", entry.getValue().getAsString());
+ }
+ }
+ Map<String, String> changedFiles = neuio.getChangedItems(oldShas);
+ for(Map.Entry<String, String> changedFile : changedFiles.entrySet()) {
+ itemShaConfig.addProperty(changedFile.getKey().substring(0, changedFile.getKey().length()-5),
+ changedFile.getValue());
+ }
+ try {
+ writeJson(itemShaConfig, itemShaLocation);
+ } catch(IOException e) {}
+
+ if(Display.isActive()) dialog.toFront();
+
+ if(changedFiles.size() <= 20) {
+ Map<String, String> downloads = neuio.getItemsDownload(changedFiles.keySet());
+
+ String startMessage = "NotEnoughUpdates: Syncing with remote repository (";
+ int downloaded = 0;
+
+ for(Map.Entry<String, String> entry : downloads.entrySet()) {
+ pane.setMessage(startMessage + (++downloaded) + "/" + downloads.size() + ")\nCurrent: " + entry.getKey());
+ dialog.pack();
+ dialog.setVisible(true);
+ if(Display.isActive()) dialog.toFront();
+
+ File item = new File(itemsLocation, entry.getKey());
+ try { item.createNewFile(); } catch(IOException e) { }
+ try(BufferedInputStream inStream = new BufferedInputStream(new URL(entry.getValue()).openStream());
+ FileOutputStream fileOutputStream = new FileOutputStream(item)) {
+ byte dataBuffer[] = new byte[1024];
+ int bytesRead;
+ while ((bytesRead = inStream.read(dataBuffer, 0, 1024)) != -1) {
+ fileOutputStream.write(dataBuffer, 0, bytesRead);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ } else {
+ //TODO: Store hard-coded value somewhere else
+ String dlUrl = "https://github.com/Moulberry/NotEnoughUpdates-REPO/archive/master.zip";
+
+ pane.setMessage("Downloading NEU Master Archive. (DL# >20)");
+ dialog.pack();
+ dialog.setVisible(true);
+ if(Display.isActive()) dialog.toFront();
+
+ File itemsZip = new File(configLocation, "neu-items-master.zip");
+ try { itemsZip.createNewFile(); } catch(IOException e) { }
+ try(BufferedInputStream inStream = new BufferedInputStream(new URL(dlUrl).openStream());
+ FileOutputStream fileOutputStream = new FileOutputStream(itemsZip)) {
+ byte dataBuffer[] = new byte[1024];
+ int bytesRead;
+ while ((bytesRead = inStream.read(dataBuffer, 0, 1024)) != -1) {
+ fileOutputStream.write(dataBuffer, 0, bytesRead);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ pane.setMessage("Unzipping NEU Master Archive.");
+ dialog.pack();
+ dialog.setVisible(true);
+ if(Display.isActive()) dialog.toFront();
+
+ unzipIgnoreFirstFolder(itemsZip.getAbsolutePath(), configLocation.getAbsolutePath());
+ }
+
+
+ dialog.dispose();
+
+ for(File f : itemsLocation.listFiles()) {
+ loadItem(f.getName().substring(0, f.getName().length()-5));
+ }
+ }
+
+ /**
+ * Loads the item in to the itemMap and also stores various words associated with this item
+ * in to titleWordMap and loreWordMap. These maps are used in the searching algorithm.
+ * @param internalName
+ */
+ private void loadItem(String internalName) {
+ try {
+ JsonObject json = getJsonFromFile(new File(itemsLocation, internalName + ".json"));
+ if(json == null) {
+ return;
+ }
+ itemMap.put(internalName, json);
+
+ if(json.has("displayname")) {
+ int wordIndex=0;
+ for(String str : json.get("displayname").getAsString().split(" ")) {
+ str = clean(str);
+ if(!titleWordMap.containsKey(str)) {
+ titleWordMap.put(str, new HashMap<>());
+ }
+ if(!titleWordMap.get(str).containsKey(internalName)) {
+ titleWordMap.get(str).put(internalName, new ArrayList<>());
+ }
+ titleWordMap.get(str).get(internalName).add(wordIndex);
+ wordIndex++;
+ }
+ }
+
+ if(json.has("lore")) {
+ int wordIndex=0;
+ for(JsonElement element : json.get("lore").getAsJsonArray()) {
+ for(String str : element.getAsString().split(" ")) {
+ str = clean(str);
+ if(!loreWordMap.containsKey(str)) {
+ loreWordMap.put(str, new HashMap<>());
+ }
+ if(!loreWordMap.get(str).containsKey(internalName)) {
+ loreWordMap.get(str).put(internalName, new ArrayList<>());
+ }
+ loreWordMap.get(str).get(internalName).add(wordIndex);
+ wordIndex++;
+ }
+ }
+ }
+ } catch(IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Searches a string for a query. This method is used to mimic the behaviour of the
+ * more complex map-based search function. This method is used for the chest-item-search feature.
+ */
+ public boolean searchString(String toSearch, String query) {
+ int lastMatch = -1;
+
+ toSearch = clean(toSearch).toLowerCase();
+ query = clean(query).toLowerCase();
+ String[] splitToSeach = toSearch.split(" ");
+ out:
+ for(String s : query.split(" ")) {
+ for(int i=0; i<splitToSeach.length; i++) {
+ if(lastMatch == -1 || lastMatch == i-1) {
+ if (splitToSeach[i].startsWith(s)) {
+ lastMatch = i;
+ continue out;
+ }
+ }
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks whether an itemstack matches a certain query, following the same rules implemented by the
+ * more complex map-based search function.
+ */
+ public boolean doesStackMatchSearch(ItemStack stack, String query) {
+ if(query.startsWith("title:")) {
+ query = query.substring(6);
+ return searchString(stack.getDisplayName(), query);
+ } else if(query.startsWith("desc:")) {
+ query = query.substring(5);
+ String lore = "";
+ NBTTagCompound tag = stack.getTagCompound();
+ if(tag != null) {
+ NBTTagCompound display = tag.getCompoundTag("display");
+ if (display.hasKey("Lore", 9)) {
+ NBTTagList list = display.getTagList("Lore", 8);
+ for (int i = 0; i < list.tagCount(); i++) {
+ lore += list.getStringTagAt(i) + " ";
+ }
+ }
+ }
+ return searchString(lore, query);
+ } else if(query.startsWith("id:")) {
+ query = query.substring(3);
+ String internalName = getInternalNameForItem(stack);
+ return query.equalsIgnoreCase(internalName);
+ } else {
+ boolean result = false;
+ if(!query.trim().contains(" ")) {
+ StringBuilder sb = new StringBuilder();
+ for(char c : query.toCharArray()) {
+ sb.append(c).append(" ");
+ }
+ result = result || searchString(stack.getDisplayName(), sb.toString());
+ }
+ result = result || searchString(stack.getDisplayName(), query);
+
+ String lore = "";
+ NBTTagCompound tag = stack.getTagCompound();
+ if(tag != null) {
+ NBTTagCompound display = tag.getCompoundTag("display");
+ if (display.hasKey("Lore", 9)) {
+ NBTTagList list = display.getTagList("Lore", 8);
+ for (int i = 0; i < list.tagCount(); i++) {
+ lore += list.getStringTagAt(i) + " ";
+ }
+ }
+ }
+
+ result = result || searchString(lore, query);
+
+ return result;
+ }
+ }
+
+ /**
+ * Returns the name of items which match a certain search query.
+ */
+ public Set<String> search(String query) {
+ LinkedHashSet<String> results = new LinkedHashSet<>();
+ if(query.startsWith("title:")) {
+ query = query.substring(6);
+ results.addAll(new TreeSet<>(search(query, titleWordMap)));
+ } else if(query.startsWith("desc:")) {
+ query = query.substring(5);
+ results.addAll(new TreeSet<>(search(query, loreWordMap)));
+ } else if(query.startsWith("id:")) {
+ query = query.substring(3);
+ results.addAll(new TreeSet<>(subMapWithKeysThatAreSuffixes(query.toUpperCase(), itemMap).keySet()));
+ } else {
+ if(!query.trim().contains(" ")) {
+ StringBuilder sb = new StringBuilder();
+ for(char c : query.toCharArray()) {
+ sb.append(c).append(" ");
+ }
+ results.addAll(new TreeSet<>(search(sb.toString(), titleWordMap)));
+ }
+ results.addAll(new TreeSet<>(search(query, titleWordMap)));
+ results.addAll(new TreeSet<>(search(query, loreWordMap)));
+ }
+ return results;
+ }
+
+ /**
+ * Splits a search query into an array of strings delimited by a space character. Then, matches the query to
+ * the start of words in the various maps (title & lore). The small query does not need to match the whole entry
+ * of the map, only the beginning. eg. "ench" and "encha" will both match "enchanted". All sub queries must
+ * follow a word matching the previous sub query. eg. "ench po" will match "enchanted pork" but will not match
+ * "pork enchanted".
+ */
+ private Set<String> search(String query, TreeMap<String, HashMap<String, List<Integer>>> wordMap) {
+ HashMap<String, List<Integer>> matches = null;
+
+ query = clean(query).toLowerCase();
+ for(String queryWord : query.split(" ")) {
+ HashMap<String, List<Integer>> matchesToKeep = new HashMap<>();
+ for(HashMap<String, List<Integer>> wordMatches : subMapWithKeysThatAreSuffixes(queryWord, wordMap).values()) {
+ if(wordMatches != null && !wordMatches.isEmpty()) {
+ if(matches == null) {
+ //Copy all wordMatches to titleMatches
+ for(String internalname : wordMatches.keySet()) {
+ if(!matchesToKeep.containsKey(internalname)) {
+ matchesToKeep.put(internalname, new ArrayList<>());
+ }
+ matchesToKeep.get(internalname).addAll(wordMatches.get(internalname));
+ }
+ } else {
+ for(String internalname : matches.keySet()) {
+ if(wordMatches.containsKey(internalname)) {
+ for(Integer newIndex : wordMatches.get(internalname)) {
+ if(matches.get(internalname).contains(newIndex-1)) {
+ if(!matchesToKeep.containsKey(internalname)) {
+ matchesToKeep.put(internalname, new ArrayList<>());
+ }
+ matchesToKeep.get(internalname).add(newIndex);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ if(matchesToKeep.isEmpty()) return new HashSet<>();
+ matches = matchesToKeep;
+ }
+
+ return matches.keySet();
+ }
+
+ /**
+ * From https://stackoverflow.com/questions/10711494/get-values-in-treemap-whose-string-keys-start-with-a-pattern
+ */
+ public <T> Map<String, T> subMapWithKeysThatAreSuffixes(String prefix, NavigableMap<String, T> map) {
+ if ("".equals(prefix)) return map;
+ String lastKey = createLexicographicallyNextStringOfTheSameLenght(prefix);
+ return map.subMap(prefix, true, lastKey, false);
+ }
+
+ String createLexicographicallyNextStringOfTheSameLenght(String input) {
+ final int lastCharPosition = input.length()-1;
+ String inputWithoutLastChar = input.substring(0, lastCharPosition);
+ char lastChar = input.charAt(lastCharPosition) ;
+ char incrementedLastChar = (char) (lastChar + 1);
+ return inputWithoutLastChar+incrementedLastChar;
+ }
+
+ private String clean(String str) {
+ return str.replaceAll("(\u00a7.)|[^0-9a-zA-Z ]", "").toLowerCase().trim();
+ }
+
+ /**
+ * Takes an item stack and produces a JsonObject. This is used in the item editor.
+ */
+ public JsonObject getJsonForItem(ItemStack stack) {
+ NBTTagCompound tag = stack.getTagCompound() == null ? new NBTTagCompound() : stack.getTagCompound();
+
+ //Item lore
+ String[] lore = new String[0];
+ if(tag.hasKey("display", 10)) {
+ NBTTagCompound display = tag.getCompoundTag("display");
+
+ if(display.hasKey("Lore", 9)) {
+ NBTTagList list = display.getTagList("Lore", 8);
+ lore = new String[list.tagCount()];
+ for(int i=0; i<list.tagCount(); i++) {
+ lore[i] = list.getStringTagAt(i);
+ }
+ }
+ }
+
+ if(stack.getDisplayName().endsWith(" Recipes")) {
+ stack.setStackDisplayName(stack.getDisplayName().substring(0, stack.getDisplayName().length()-8));
+ }
+
+ if(lore.length > 0 && (lore[lore.length-1].endsWith("Click to view recipes!") ||
+ lore[lore.length-1].endsWith("Click to view recipe!"))) {
+ String[] lore2 = new String[lore.length-2];
+ System.arraycopy(lore, 0, lore2, 0, lore.length-2);
+ lore = lore2;
+ }
+
+ JsonObject json = new JsonObject();
+ json.addProperty("itemid", stack.getItem().getRegistryName());
+ json.addProperty("displayname", stack.getDisplayName());
+ json.addProperty("nbttag", tag.toString());
+ json.addProperty("damage", stack.getItemDamage());
+
+ JsonArray jsonlore = new JsonArray();
+ for(String line : lore) {
+ jsonlore.add(new JsonPrimitive(line));
+ }
+ json.add("lore", jsonlore);
+
+ return json;
+ }
+
+ public String getInternalNameForItem(ItemStack stack) {
+ NBTTagCompound tag = stack.getTagCompound();
+ //Internal id
+ if(tag != null && tag.hasKey("ExtraAttributes", 10)) {
+ NBTTagCompound ea = tag.getCompoundTag("ExtraAttributes");
+
+ if(ea.hasKey("id", 8)) {
+ return ea.getString("id");
+ }
+ }
+ return null;
+ }
+
+ //Currently unused in production.
+ public void writeItemToFile(ItemStack stack) {
+ String internalname = getInternalNameForItem(stack);
+
+ if(internalname == null) {
+ return;
+ }
+
+ JsonObject json = getJsonForItem(stack);
+ json.addProperty("internalname", internalname);
+ json.addProperty("clickcommand", "viewrecipe");
+ json.addProperty("modver", NotEnoughUpdates.VERSION);
+
+ if(!json.has("internalname")) {
+ return;
+ }
+
+ try {
+ writeJson(json, new File(itemsLocation, internalname+".json"));
+ } catch (IOException e) {}
+
+ loadItem(internalname);
+ }
+
+ public JsonObject createItemJson(String internalname, String itemid, String displayname, String[] lore, String[] info,
+ String clickcommand, int damage, NBTTagCompound nbttag) {
+ if(internalname == null || internalname.isEmpty()) {
+ return null;
+ }
+
+ JsonObject json = new JsonObject();
+ json.addProperty("internalname", internalname);
+ json.addProperty("itemid", itemid);
+ json.addProperty("displayname", displayname);
+ json.addProperty("clickcommand", clickcommand);
+ json.addProperty("damage", damage);
+ json.addProperty("nbttag", nbttag.toString());
+ json.addProperty("modver", NotEnoughUpdates.VERSION);
+
+ if(info != null && info.length > 0) {
+ JsonArray jsoninfo = new JsonArray();
+ for (String line : info) {
+ jsoninfo.add(new JsonPrimitive(line));
+ }
+ json.add("info", jsoninfo);
+ }
+
+ JsonArray jsonlore = new JsonArray();
+ for(String line : lore) {
+ System.out.println("Lore:"+line);
+ jsonlore.add(new JsonPrimitive(line));
+ }
+ json.add("lore", jsonlore);
+
+ return json;
+ }
+
+ public boolean writeItemJson(String internalname, String itemid, String displayname, String[] lore, String[] info,
+ String clickcommand, int damage, NBTTagCompound nbttag) {
+ JsonObject json = createItemJson(internalname, itemid, displayname, lore, info, clickcommand, damage, nbttag);
+ if(json == null) {
+ return false;
+ }
+
+ try {
+ writeJsonDefaultDir(json, internalname+".json");
+ } catch(IOException e) {
+ return false;
+ }
+
+ loadItem(internalname);
+ return true;
+ }
+
+ public boolean uploadItemJson(String internalname, String itemid, String displayname, String[] lore, String[] info,
+ String clickcommand, int damage, NBTTagCompound nbttag) {
+ JsonObject json = createItemJson(internalname, itemid, displayname, lore, info, clickcommand, damage, nbttag);
+ if(json == null) {
+ return false;
+ }
+
+ String username = Minecraft.getMinecraft().thePlayer.getName();
+ String newBranchName = UUID.randomUUID().toString().substring(0, 8) + "-" + internalname + "-" + username;
+ String prTitle = internalname + "-" + username;
+ String prBody = "Internal name: " + internalname + "\nSubmitted by: " + username;
+ String file = "items/"+internalname+".json";
+ if(!neuio.createNewRequest(newBranchName, prTitle, prBody, file, gson.toJson(json))) {
+ return false;
+ }
+
+ try {
+ writeJsonDefaultDir(json, internalname+".json");
+ } catch(IOException e) {
+ return false;
+ }
+
+ loadItem(internalname);
+ return true;
+ }
+
+ private void writeJson(JsonObject json, File file) throws IOException {
+ file.createNewFile();
+
+ try(BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
+ writer.write(gson.toJson(json));
+ }
+ }
+
+ private void writeJsonDefaultDir(JsonObject json, String filename) throws IOException {
+ File file = new File(itemsLocation, filename);
+ writeJson(json, file);
+ }
+
+ public TreeMap<String, JsonObject> getItemInformation() {
+ return itemMap;
+ }
+
+ /**
+ * Stolen from https://www.journaldev.com/960/java-unzip-file-example
+ */
+ private static void unzipIgnoreFirstFolder(String zipFilePath, String destDir) {
+ File dir = new File(destDir);
+ // create output directory if it doesn't exist
+ if(!dir.exists()) dir.mkdirs();
+ FileInputStream fis;
+ //buffer for read and write data to file
+ byte[] buffer = new byte[1024];
+ try {
+ fis = new FileInputStream(zipFilePath);
+ ZipInputStream zis = new ZipInputStream(fis);
+ ZipEntry ze = zis.getNextEntry();
+ while(ze != null){
+ if(!ze.isDirectory()) {
+ String fileName = ze.getName();
+ fileName = fileName.substring(fileName.split("/")[0].length()+1);
+ File newFile = new File(destDir + File.separator + fileName);
+ //create directories for sub directories in zip
+ new File(newFile.getParent()).mkdirs();
+ FileOutputStream fos = new FileOutputStream(newFile);
+ int len;
+ while ((len = zis.read(buffer)) > 0) {
+ fos.write(buffer, 0, len);
+ }
+ fos.close();
+ }
+ //close this ZipEntry
+ zis.closeEntry();
+ ze = zis.getNextEntry();
+ }
+ //close last ZipEntry
+ zis.closeEntry();
+ zis.close();
+ fis.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+}
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/NEUOverlay.java b/src/main/java/io/github/moulberry/notenoughupdates/NEUOverlay.java
new file mode 100644
index 00000000..b86ad932
--- /dev/null
+++ b/src/main/java/io/github/moulberry/notenoughupdates/NEUOverlay.java
@@ -0,0 +1,804 @@
+package io.github.moulberry.notenoughupdates;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.mojang.realmsclient.gui.ChatFormatting;
+import io.github.moulberry.notenoughupdates.itemeditor.NEUItemEditor;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.*;
+import net.minecraft.client.gui.inventory.GuiContainer;
+import net.minecraft.client.renderer.*;
+import net.minecraft.client.renderer.entity.RenderItem;
+import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
+import net.minecraft.init.Blocks;
+import net.minecraft.init.Items;
+import net.minecraft.inventory.Slot;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.*;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraft.util.ResourceLocation;
+import net.minecraftforge.fml.client.config.GuiUtils;
+import org.lwjgl.input.Keyboard;
+import org.lwjgl.input.Mouse;
+import org.lwjgl.opengl.GL11;
+
+import java.awt.*;
+import java.lang.reflect.Field;
+import java.util.*;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiConsumer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class NEUOverlay extends Gui {
+
+ private NEUManager manager;
+
+ private ResourceLocation itemPaneTabArrow = new ResourceLocation("notenoughupdates:item_pane_tab_arrow.png");
+ private ResourceLocation prev = new ResourceLocation("notenoughupdates:prev_pow2.png");
+ private ResourceLocation next = new ResourceLocation("notenoughupdates:next_pow2.png");
+ private ResourceLocation item_edit = new ResourceLocation("notenoughupdates:item_edit.png");
+
+ private final int searchBarXSize = 200;
+ private final int searchBarYOffset = 10;
+ private final int searchBarYSize = 40;
+ private final int searchBarPadding = 2;
+
+ private final int boxPadding = 15;
+ private final int itemPadding = 4;
+ private final int itemSize = 16;
+
+ private String informationPaneTitle;
+ private String[] informationPane;
+
+ private boolean allowItemEditing;
+
+ private LinkedHashMap<String, JsonObject> searchedItems = null;
+
+ private boolean itemPaneOpen = false;
+
+ private int page = 0;
+
+ private LerpingFloat itemPaneOffsetFactor = new LerpingFloat(1);
+ private LerpingInteger itemPaneTabOffset = new LerpingInteger(20, 50);
+ private LerpingFloat infoPaneOffsetFactor = new LerpingFloat(0);
+
+ private boolean searchMode = false;
+ private long millisLastLeftClick = 0;
+
+ private boolean searchBarHasFocus = false;
+ GuiTextField textField = new GuiTextField(0, null, 0, 0, 0, 0);
+
+ public NEUOverlay(NEUManager manager) {
+ this.manager = manager;
+ textField.setFocused(true);
+ textField.setCanLoseFocus(false);
+
+ allowItemEditing = manager.getAllowEditing();
+ }
+
+ public void reset() {
+ searchBarHasFocus = false;
+ itemPaneOpen = searchMode;
+ itemPaneOffsetFactor.setValue(1);
+ itemPaneTabOffset.setValue(20);
+ }
+
+ /**
+ * Handles the mouse input, cancelling the forge event if a NEU gui element is clicked.
+ */
+ public boolean mouseInput() {
+ ScaledResolution scaledresolution = new ScaledResolution(Minecraft.getMinecraft());
+ int width = scaledresolution.getScaledWidth();
+ int height = scaledresolution.getScaledHeight();
+
+ int mouseX = Mouse.getX() / scaledresolution.getScaleFactor();
+ int mouseY = height - Mouse.getY() / scaledresolution.getScaleFactor();
+
+ //Unfocuses the search bar by default. Search bar is focused if the click is on the bar itself.
+ if(Mouse.getEventButtonState()) setSearchBarFocus(false);
+
+ //Item selection (right) gui
+ if(mouseX > width*itemPaneOffsetFactor.getValue()) {
+ if(!Mouse.getEventButtonState()) return true; //End early if the mouse isn't pressed, but still cancel event.
+
+ AtomicBoolean clickedItem = new AtomicBoolean(false);
+ iterateItemSlots((x, y) -> {
+ if(mouseX >= x-1 && mouseX <= x+itemSize+1) {
+ if(mouseY >= y-1 && mouseY <= y+itemSize+1) {
+ clickedItem.set(true);
+ //TODO: Do something when clicking on items :)
+ int id = getSlotId(x, y);
+ JsonObject item = getSearchedItemPage(id);
+ if (item != null) {
+ if(item.has("clickcommand") && Mouse.getEventButton() == 0) {
+ String clickcommand = item.get("clickcommand").getAsString();
+
+ if(clickcommand.equals("viewrecipe")) {
+ Minecraft.getMinecraft().thePlayer.sendChatMessage(
+ "/" + clickcommand + " " + item.get("internalname").getAsString().toUpperCase());
+ } else if(clickcommand.equals("viewpotion")) {
+
+ Minecraft.getMinecraft().thePlayer.sendChatMessage(
+ "/" + clickcommand + " " + item.get("internalname").getAsString().toLowerCase());
+ }
+ } else if(item.has("info") && Mouse.getEventButton() == 1) {
+ JsonArray lore = item.get("info").getAsJsonArray();
+ String[] loreA = new String[lore.size()];
+ for(int i=0; i<lore.size(); i++) loreA[i] = lore.get(i).getAsString();
+ displayInformationPane(item.get("displayname").getAsString(), loreA);
+ } else if(Mouse.getEventButton() == 3) {
+ textField.setText("itemid:"+item.get("internalname").getAsString());
+ }
+ }
+ }
+ }
+ });
+ if(!clickedItem.get()) {
+ int leftSide = (int)(width*itemPaneOffsetFactor.getValue());
+
+ if(mouseY > boxPadding && mouseY < boxPadding+searchBarYSize/scaledresolution.getScaleFactor()) {
+ int leftPrev = leftSide+boxPadding+getItemBoxXPadding();
+ if(mouseX > leftPrev && mouseX < leftPrev+120/scaledresolution.getScaleFactor()) { //"Previous" button
+ page--;
+ }
+ int rightNext = leftSide+width/3-boxPadding-getItemBoxXPadding();
+ if(mouseX > rightNext-120/scaledresolution.getScaleFactor() && mouseX < rightNext) {
+ page++;
+ }
+ }
+ }
+ return true;
+ }
+
+ //Search bar
+ if(mouseX >= width/2 - searchBarXSize/2 && mouseX <= width/2 + searchBarXSize/2) {
+ if(mouseY >= height - searchBarYOffset - searchBarYSize/scaledresolution.getScaleFactor() &&
+ mouseY <= height - searchBarYOffset) {
+ if(Mouse.getEventButtonState()) {
+ setSearchBarFocus(true);
+ if(Mouse.getEventButton() == 1) { //Right mouse button down
+ textField.setText("");
+ updateSearch();
+ } else {
+ if(System.currentTimeMillis() - millisLastLeftClick < 300) {
+ searchMode = !searchMode;
+ }
+ textField.setCursorPosition(getClickedIndex(mouseX, mouseY));
+ millisLastLeftClick = System.currentTimeMillis();
+ }
+ }
+ return true;
+ }
+ }
+
+ int paddingUnscaled = searchBarPadding/scaledresolution.getScaleFactor();
+ int topTextBox = height - searchBarYOffset - searchBarYSize/scaledresolution.getScaleFactor();
+ int iconSize = searchBarYSize/scaledresolution.getScaleFactor()+paddingUnscaled*2;
+ if(paddingUnscaled < 1) paddingUnscaled = 1;
+ if(mouseX > width/2 + searchBarXSize/2 + paddingUnscaled*6 &&
+ mouseX < width/2 + searchBarXSize/2 + paddingUnscaled*6 + iconSize) {
+ if(mouseY > topTextBox - paddingUnscaled && mouseY < topTextBox - paddingUnscaled + iconSize) {
+ if(Mouse.getEventButtonState()) {
+ allowItemEditing = !allowItemEditing;
+ manager.setAllowEditing(allowItemEditing);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public void displayInformationPane(String title, String[] info) {
+ informationPaneTitle = title;
+
+ if(info == null || info.length == 0) {
+ informationPane = new String[]{"\u00A77No additional information."};
+ } else {
+ informationPane = info;
+ }
+ infoPaneOffsetFactor.setTarget(1/3f);
+ infoPaneOffsetFactor.resetTimer();
+ }
+
+ public int getClickedIndex(int mouseX, int mouseY) {
+ ScaledResolution scaledresolution = new ScaledResolution(Minecraft.getMinecraft());
+ int width = scaledresolution.getScaledWidth();
+ int height = scaledresolution.getScaledHeight();
+
+ int xComp = mouseX - (width/2 - searchBarXSize/2 + 5);
+
+ String trimmed = Minecraft.getMinecraft().fontRendererObj.trimStringToWidth(textField.getText(), xComp);
+ int linePos = trimmed.length();
+ if(linePos != textField.getText().length()) {
+ char after = textField.getText().charAt(linePos);
+ int trimmedWidth = Minecraft.getMinecraft().fontRendererObj.getStringWidth(trimmed);
+ int charWidth = Minecraft.getMinecraft().fontRendererObj.getCharWidth(after);
+ if(trimmedWidth + charWidth/2 < xComp-5) {
+ linePos++;
+ }
+ }
+ return linePos;
+ }
+
+ public void setSearchBarFocus(boolean focus) {
+ if(focus) {
+ itemPaneOpen = true;
+ }
+ searchBarHasFocus = focus;
+ }
+
+ /**
+ * Handles the keyboard input, cancelling the forge event if the search bar has focus.
+ */
+ public boolean keyboardInput() {
+ if(searchBarHasFocus && Keyboard.getEventKey() == 1 && Keyboard.getEventKeyState()) {
+ searchBarHasFocus = false;
+ if(!textField.getText().isEmpty()) {
+ return true;
+ }
+ }
+
+ if(searchBarHasFocus && Keyboard.getEventKeyState()) {
+ if(textField.textboxKeyTyped(Keyboard.getEventCharacter(), Keyboard.getEventKey())) {
+ if(textField.getText().isEmpty()) {
+ searchedItems = null;
+ } else {
+ updateSearch();
+ }
+ }
+ }
+
+ if(allowItemEditing) {
+ if(Keyboard.getEventCharacter() == 'k' && Keyboard.getEventKeyState()) {
+ Slot slot = ((GuiContainer)Minecraft.getMinecraft().currentScreen).getSlotUnderMouse();
+ if(slot != null) {
+ ItemStack hover = slot.getStack();
+ if(hover != null) {
+ Minecraft.getMinecraft().displayGuiScreen(new NEUItemEditor(manager,
+ manager.getInternalNameForItem(hover), manager.getJsonForItem(hover)));
+ //manager.writeItemToFile(hover);
+ }
+ } else {
+ ScaledResolution scaledresolution = new ScaledResolution(Minecraft.getMinecraft());
+ int height = scaledresolution.getScaledHeight();
+
+ int mouseX = Mouse.getX() / scaledresolution.getScaleFactor();
+ int mouseY = height - Mouse.getY() / scaledresolution.getScaleFactor();
+
+ iterateItemSlots((x, y) -> {
+ if (mouseX >= x - 1 && mouseX <= x + itemSize + 1) {
+ if (mouseY >= y - 1 && mouseY <= y + itemSize + 1) {
+ int id = getSlotId(x, y);
+ JsonObject item = getSearchedItemPage(id);
+
+ if(item != null) {
+ Minecraft.getMinecraft().displayGuiScreen(new NEUItemEditor(manager,
+ item.get("internalname").getAsString(), item));
+ }
+ }
+ }
+ });
+ }
+ }
+ }
+
+ return searchBarHasFocus; //Cancels keyboard events if the search bar has focus
+ }
+
+ public void updateSearch() {
+ if(searchedItems==null) searchedItems = new LinkedHashMap<>();
+ searchedItems.clear();
+ Set<String> itemsMatch = manager.search(textField.getText());
+ for(String item : itemsMatch) {
+ searchedItems.put(item, manager.getItemInformation().get(item));
+ }
+ }
+
+ public Collection<JsonObject> getSearchedItems() {
+ if(searchedItems==null) {
+ return manager.getItemInformation().values();
+ } else {
+ return searchedItems.values();
+ }
+ }
+
+ public JsonObject getSearchedItemPage(int index) {
+ if(index < getSlotsXSize()*getSlotsYSize()) {
+ int actualIndex = index + getSlotsXSize()*getSlotsYSize()*page;
+ if(actualIndex < getSearchedItems().size()) {
+ return (JsonObject) (getSearchedItems().toArray()[actualIndex]);
+ } else {
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+
+ public int getItemBoxXPadding() {
+ ScaledResolution scaledresolution = new ScaledResolution(Minecraft.getMinecraft());
+ int width = scaledresolution.getScaledWidth();
+ return (((int)(width-width*itemPaneOffsetFactor.getValue())-2*boxPadding)%(itemSize+itemPadding)+itemPadding)/2;
+ }
+
+ /**
+ * Iterates through all the item slots in the right panel and calls a biconsumer for each slot with
+ * arguments equal to the slot's x and y position respectively. This is used in order to prevent
+ * code duplication issues.
+ */
+ public void iterateItemSlots(BiConsumer<Integer, Integer> itemSlotConsumer) {
+ ScaledResolution scaledresolution = new ScaledResolution(Minecraft.getMinecraft());
+ int width = scaledresolution.getScaledWidth();
+ int height = scaledresolution.getScaledHeight();
+
+ int itemBoxXPadding = getItemBoxXPadding();
+ int itemBoxYPadding = ((height-2*boxPadding)%(itemSize+itemPadding)+itemPadding)/2;
+
+ int xStart = (int)(width*itemPaneOffsetFactor.getValue())+boxPadding+itemBoxXPadding;
+ int yStart = boxPadding+searchBarYSize/scaledresolution.getScaleFactor()+itemBoxYPadding;
+ int xEnd = (int)(width*itemPaneOffsetFactor.getValue())+width/3-boxPadding-itemSize;
+ int yEnd = height-boxPadding-itemSize;
+
+ //Render the items, displaying the tooltip if the cursor is over the item
+ for(int y = yStart; y < yEnd; y+=itemSize+itemPadding) {
+ for(int x = xStart; x < xEnd; x+=itemSize+itemPadding) {
+ itemSlotConsumer.accept(x, y);
+ }
+ }
+ }
+
+ /**
+ * Calculates the number of horizontal item slots.
+ */
+ public int getSlotsXSize() {
+ ScaledResolution scaledresolution = new ScaledResolution(Minecraft.getMinecraft());
+ int width = scaledresolution.getScaledWidth();
+
+ int itemBoxXPadding = (((int)(width-width*itemPaneOffsetFactor.getValue())-2*boxPadding)%(itemSize+itemPadding)+itemPadding)/2;
+ int xStart = (int)(width*itemPaneOffsetFactor.getValue())+boxPadding+itemBoxXPadding;
+ int xEnd = (int)(width*itemPaneOffsetFactor.getValue())+width/3-boxPadding-itemSize;
+
+ return (int)Math.ceil((xEnd - xStart)/((float)(itemSize+itemPadding)));
+ }
+
+ /**
+ * Calculates the number of vertical item slots.
+ */
+ public int getSlotsYSize() {
+ ScaledResolution scaledresolution = new ScaledResolution(Minecraft.getMinecraft());
+ int height = scaledresolution.getScaledHeight();
+
+ int itemBoxYPadding = ((height-2*boxPadding)%(itemSize+itemPadding)+itemPadding)/2;
+ int yStart = boxPadding+searchBarYSize/scaledresolution.getScaleFactor()+itemBoxYPadding;
+ int yEnd = height-boxPadding-itemSize;
+
+ return (int)Math.ceil((yEnd - yStart)/((float)(itemSize+itemPadding)));
+ }
+
+ public int getMaxPages() {
+ if(getSearchedItems().size() == 0) return 1;
+ return (int)Math.ceil(getSearchedItems().size()/(float)getSlotsYSize()/getSlotsXSize());
+ }
+
+ /**
+ * Takes in the x and y coordinates of a slot and returns the id of that slot.
+ */
+ public int getSlotId(int x, int y) {
+ ScaledResolution scaledresolution = new ScaledResolution(Minecraft.getMinecraft());
+ int width = scaledresolution.getScaledWidth();
+ int height = scaledresolution.getScaledHeight();
+
+ int itemBoxXPadding = (((int)(width-width*itemPaneOffsetFactor.getValue())-2*boxPadding)%(itemSize+itemPadding)+itemPadding)/2;
+ int itemBoxYPadding = ((height-2*boxPadding)%(itemSize+itemPadding)+itemPadding)/2;
+
+ int xStart = (int)(width*itemPaneOffsetFactor.getValue())+boxPadding+itemBoxXPadding;
+ int yStart = boxPadding+searchBarYSize/scaledresolution.getScaleFactor()+itemBoxYPadding;
+
+ int xIndex = (x-xStart)/(itemSize+itemPadding);
+ int yIndex = (y-yStart)/(itemSize+itemPadding);
+ return xIndex + yIndex*getSlotsXSize();
+ }
+
+ /**
+ * Renders the search bar, item selection (right) and item info (left) gui elements.
+ */
+ public void render(float partialTicks, int mouseX, int mouseY) {
+ FontRenderer fr = Minecraft.getMinecraft().fontRendererObj;
+ ScaledResolution scaledresolution = new ScaledResolution(Minecraft.getMinecraft());
+ int width = scaledresolution.getScaledWidth();
+ int height = scaledresolution.getScaledHeight();
+
+ if(searchMode && textField.getText().length() > 0) {
+ try {
+ GuiContainer inv = (GuiContainer) Minecraft.getMinecraft().currentScreen;
+ Field guiLeft = GuiContainer.class.getDeclaredField("field_147003_i");
+ Field guiTop = GuiContainer.class.getDeclaredField("field_147009_r");
+ guiLeft.setAccessible(true);
+ guiTop.setAccessible(true);
+ int guiLeftI = (int) guiLeft.get(inv);
+ int guiTopI = (int) guiTop.get(inv);
+
+ GL11.glPushMatrix();
+ GL11.glTranslatef(0, 0, 300);
+ int overlay = new Color(50, 50, 50, 150).getRGB();
+ for(Slot slot : inv.inventorySlots.inventorySlots) {
+ if(slot.getStack() == null || !manager.doesStackMatchSearch(slot.getStack(), textField.getText())) {
+ drawRect(guiLeftI+slot.xDisplayPosition, guiTopI+slot.yDisplayPosition,
+ guiLeftI+slot.xDisplayPosition+16, guiTopI+slot.yDisplayPosition+16,
+ overlay);
+ }
+ }
+ if(inv.getSlotUnderMouse() != null) {
+ ItemStack stack = inv.getSlotUnderMouse().getStack();
+ if(stack != null) {
+ List<String> list = stack.getTooltip(Minecraft.getMinecraft().thePlayer,
+ Minecraft.getMinecraft().gameSettings.advancedItemTooltips);
+
+ for (int i = 0; i < list.size(); ++i){
+ if (i == 0){
+ list.set(i, stack.getRarity().rarityColor + (String)list.get(i));
+ } else {
+ list.set(i, EnumChatFormatting.GRAY + (String)list.get(i));
+ }
+ }
+
+ GuiUtils.drawHoveringText(list, mouseX, mouseY, width, height, -1, Minecraft.getMinecraft().fontRendererObj);
+ }
+ }
+ GL11.glPopMatrix();
+ } catch (NoSuchFieldException | IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ if(itemPaneOpen) {
+ if(itemPaneTabOffset.getValue() == 0) {
+ if(itemPaneOffsetFactor.getTarget() != 2/3f) {
+ itemPaneOffsetFactor.setTarget(2/3f);
+ itemPaneOffsetFactor.resetTimer();
+ }
+ } else {
+ if(itemPaneTabOffset.getTarget() != 0) {
+ itemPaneTabOffset.setTarget(0);
+ itemPaneTabOffset.resetTimer();
+ }
+ }
+ } else {
+ if(itemPaneOffsetFactor.getValue() == 1) {
+ if(itemPaneTabOffset.getTarget() != 20) {
+ itemPaneTabOffset.setTarget(20);
+ itemPaneTabOffset.resetTimer();
+ }
+ } else {
+ if(itemPaneOffsetFactor.getTarget() != 1f) {
+ itemPaneOffsetFactor.setTarget(1f);
+ itemPaneOffsetFactor.resetTimer();
+ }
+ }
+ }
+
+ itemPaneOffsetFactor.tick();
+ itemPaneTabOffset.tick();
+ infoPaneOffsetFactor.tick();
+
+ if(page > getMaxPages()-1) page = getMaxPages()-1;
+ if(page < 0) page = 0;
+
+ GlStateManager.disableLighting();
+
+ /**
+ * Search bar
+ */
+ int paddingUnscaled = searchBarPadding/scaledresolution.getScaleFactor();
+ if(paddingUnscaled < 1) paddingUnscaled = 1;
+
+ int topTextBox = height - searchBarYOffset - searchBarYSize/scaledresolution.getScaleFactor();
+
+ //Search bar background
+ drawRect(width/2 - searchBarXSize/2 - paddingUnscaled,
+ topTextBox - paddingUnscaled,
+ width/2 + searchBarXSize/2 + paddingUnscaled,
+ height - searchBarYOffset + paddingUnscaled, searchMode ? Color.YELLOW.getRGB() : Color.WHITE.getRGB());
+ drawRect(width/2 - searchBarXSize/2,
+ topTextBox,
+ width/2 + searchBarXSize/2,
+ height - searchBarYOffset, Color.BLACK.getRGB());
+
+ //Item editor enable button
+ int iconSize = searchBarYSize/scaledresolution.getScaleFactor()+paddingUnscaled*2;
+ Minecraft.getMinecraft().getTextureManager().bindTexture(item_edit);
+ drawRect(width/2 + searchBarXSize/2 + paddingUnscaled*6,
+ topTextBox - paddingUnscaled,
+ width/2 + searchBarXSize/2 + paddingUnscaled*6 + iconSize,
+ topTextBox - paddingUnscaled + iconSize, Color.WHITE.getRGB());
+
+ drawRect(width/2 + searchBarXSize/2 + paddingUnscaled*7,
+ topTextBox,
+ width/2 + searchBarXSize/2 + paddingUnscaled*5 + iconSize,
+ topTextBox - paddingUnscaled*2 + iconSize, allowItemEditing ? Color.GREEN.getRGB() : Color.RED.getRGB());
+ GlStateManager.color(1f, 1f, 1f, 1f);
+ drawTexturedRect(width/2 + searchBarXSize/2 + paddingUnscaled*6, topTextBox - paddingUnscaled, iconSize, iconSize);
+ GlStateManager.bindTexture(0);
+
+ if(mouseX > width/2 + searchBarXSize/2 + paddingUnscaled*6 &&
+ mouseX < width/2 + searchBarXSize/2 + paddingUnscaled*6 + iconSize) {
+ if(mouseY > topTextBox - paddingUnscaled && mouseY < topTextBox - paddingUnscaled + iconSize) {
+ GuiUtils.drawHoveringText(Arrays.asList(
+ EnumChatFormatting.GOLD.toString()+EnumChatFormatting.BOLD+"Enable item editing",
+ EnumChatFormatting.RED+"Warning: "+EnumChatFormatting.YELLOW+"Uploading fake, " +
+ "misinformative or misleading information using the item editor may result in your " +
+ "account being banned from using this mod.",
+ EnumChatFormatting.GREEN.toString()+EnumChatFormatting.BOLD+"Press k on an any item to use the item editor."),
+ mouseX, mouseY, width, height, 250, Minecraft.getMinecraft().fontRendererObj);
+ GlStateManager.disableLighting();
+ }
+ }
+
+ //Search bar text
+ fr.drawString(textField.getText(), width/2 - searchBarXSize/2 + 5,
+ topTextBox+(searchBarYSize/scaledresolution.getScaleFactor()-8)/2, Color.WHITE.getRGB());
+
+ //Determines position of cursor. Cursor blinks on and off every 500ms.
+ if(searchBarHasFocus && System.currentTimeMillis()%1000>500) {
+ String textBeforeCursor = textField.getText().substring(0, textField.getCursorPosition());
+ int textBeforeCursorWidth = fr.getStringWidth(textBeforeCursor);
+ drawRect(width/2 - searchBarXSize/2 + 5 + textBeforeCursorWidth,
+ topTextBox+(searchBarYSize/scaledresolution.getScaleFactor()-8)/2-1,
+ width/2 - searchBarXSize/2 + 5 + textBeforeCursorWidth+1,
+ topTextBox+(searchBarYSize/scaledresolution.getScaleFactor()-8)/2+9, Color.WHITE.getRGB());
+ }
+
+ String selectedText = textField.getSelectedText();
+ if(!selectedText.isEmpty()) {
+ int selectionWidth = fr.getStringWidth(selectedText);
+
+ int leftIndex = textField.getCursorPosition() < textField.getSelectionEnd() ?
+ textField.getCursorPosition() : textField.getSelectionEnd();
+ String textBeforeSelection = textField.getText().substring(0, leftIndex);
+ int textBeforeSelectionWidth = fr.getStringWidth(textBeforeSelection);
+
+ drawRect(width/2 - searchBarXSize/2 + 5 + textBeforeSelectionWidth,
+ topTextBox+(searchBarYSize/scaledresolution.getScaleFactor()-8)/2-1,
+ width/2 - searchBarXSize/2 + 5 + textBeforeSelectionWidth + selectionWidth,
+ topTextBox+(searchBarYSize/scaledresolution.getScaleFactor()-8)/2+9, Color.LIGHT_GRAY.getRGB());
+
+ fr.drawString(selectedText,
+ width/2 - searchBarXSize/2 + 5 + textBeforeSelectionWidth,
+ topTextBox+(searchBarYSize/scaledresolution.getScaleFactor()-8)/2, Color.BLACK.getRGB());
+ }
+
+
+ /**
+ * Item selection (right) gui element rendering
+ */
+ Color bg = new Color(128, 128, 128, 50);
+ int leftSide = (int)(width*itemPaneOffsetFactor.getValue());
+ int rightSide = leftSide+width/3-boxPadding-getItemBoxXPadding();
+ drawRect(leftSide+boxPadding-5, boxPadding-5,
+ leftSide+width/3-boxPadding+5, height-boxPadding+5, bg.getRGB());
+
+ drawRect(leftSide+boxPadding+getItemBoxXPadding()-1, boxPadding,
+ rightSide+1,
+ boxPadding+searchBarYSize/scaledresolution.getScaleFactor(), Color.GRAY.getRGB());
+
+ Minecraft.getMinecraft().getTextureManager().bindTexture(prev);
+ GlStateManager.color(1f, 1f, 1f, 1f);
+ drawTexturedRect(leftSide+boxPadding+getItemBoxXPadding(), boxPadding,
+ 120/scaledresolution.getScaleFactor(), searchBarYSize/scaledresolution.getScaleFactor());
+ GlStateManager.bindTexture(0);
+
+
+ Minecraft.getMinecraft().getTextureManager().bindTexture(next);
+ GlStateManager.color(1f, 1f, 1f, 1f);
+ drawTexturedRect(rightSide-120/scaledresolution.getScaleFactor(), boxPadding,
+ 120/scaledresolution.getScaleFactor(), searchBarYSize/scaledresolution.getScaleFactor());
+ GlStateManager.bindTexture(0);
+
+ int offset = 1;
+ if(scaledresolution.getScaleFactor()==4) offset = 0;
+
+ String pageText = EnumChatFormatting.BOLD+"Page: " + (page+1) + "/" + getMaxPages();
+ fr.drawString(pageText, leftSide+width/6-fr.getStringWidth(pageText)/2-1,
+ boxPadding+(searchBarYSize/scaledresolution.getScaleFactor())/2-4+offset, Color.BLACK.getRGB());
+
+ //Tab
+ Minecraft.getMinecraft().getTextureManager().bindTexture(itemPaneTabArrow);
+ GlStateManager.color(1f, 1f, 1f, 0.3f);
+ drawTexturedRect(width-itemPaneTabOffset.getValue(), height/2 - 50, 20, 100);
+ GlStateManager.bindTexture(0);
+
+ if(mouseX > width-itemPaneTabOffset.getValue() && mouseY > height/2 - 50
+ && mouseY < height/2 + 50) {
+ itemPaneOpen = true;
+ }
+
+ //Atomic integer used so that below lambda doesn't complain about non-effectively-final variable
+ AtomicReference<JsonObject> tooltipToDisplay = new AtomicReference<>(null);
+
+ //Iterate through all item slots and display the appropriate item
+ iterateItemSlots((x, y) -> {
+ int id = getSlotId(x, y);
+ JsonObject json = getSearchedItemPage(id);
+ if(json == null) {
+ return;
+ }
+
+ drawRect(x-1, y-1, x+itemSize+1, y+itemSize+1, Color.GRAY.getRGB());
+ ItemStack stack = new ItemStack(Item.itemRegistry.getObject(
+ new ResourceLocation(json.get("itemid").getAsString())));
+
+ if(stack.getItem() == null) {
+ stack = new ItemStack(Items.diamond, 1, 10); //Purple broken texture item
+ } else {
+ if(json.has("damage")) {
+ stack.setItemDamage(json.get("damage").getAsInt());
+ }
+
+ if(json.has("nbttag")) {
+ try {
+ NBTTagCompound tag = JsonToNBT.getTagFromJson(json.get("nbttag").getAsString());
+ stack.setTagCompound(tag);
+ } catch(NBTException e) {
+ }
+ }
+ }
+
+ //Integer.toString(getSlotId(x, y))
+ drawItemStack(stack, x, y, null);
+
+ if(mouseX > x-1 && mouseX < x+itemSize+1) {
+ if(mouseY > y-1 && mouseY < y+itemSize+1) {
+ tooltipToDisplay.set(json);
+ }
+ }
+ });
+
+ //Render tooltip
+ JsonObject json = tooltipToDisplay.get();
+ if(json != null) {
+ List<String> text = new ArrayList<>();
+ text.add(json.get("displayname").getAsString());
+ JsonArray lore = json.get("lore").getAsJsonArray();
+ for(int i=0; i<lore.size(); i++) {
+ text.add(lore.get(i).getAsString());
+ }
+
+ boolean hasClick = false;
+ boolean hasInfo = false;
+ if(json.has("clickcommand") && !json.get("clickcommand").getAsString().isEmpty()) {
+ hasClick = true;
+ }
+ if(json.has("info") && json.get("info").getAsJsonArray().size() > 0) {
+ hasInfo = true;
+ }
+
+ if(hasClick || hasInfo) text.add("");
+ if(hasClick) text.add("\u00A7e\u00A7lLeft click to view recipe!");
+ if(hasInfo) text.add("\u00A7e\u00A7lRight click to view additional information!");
+
+ GuiUtils.drawHoveringText(text, mouseX, mouseY, width, height, -1, fr);
+ GlStateManager.disableLighting();
+ }
+
+ /**
+ * Item info (left) gui element rendering
+ */
+
+ rightSide = (int)(width*getInfoPaneOffsetFactor());
+ leftSide = rightSide - width/3;
+
+ drawRect(leftSide+boxPadding-5, boxPadding-5, rightSide-boxPadding+5, height-boxPadding+5, bg.getRGB());
+ if(informationPane != null && informationPaneTitle != null) {
+ int titleLen = fr.getStringWidth(informationPaneTitle);
+ fr.drawString(informationPaneTitle, (leftSide+rightSide-titleLen)/2, boxPadding + 5, Color.WHITE.getRGB());
+
+ int yOff = 20;
+ for(int i=0; i<informationPane.length; i++) {
+ String line = informationPane[i];
+
+ String excess;
+ String trimmed = trimToWidth(line, width*1/3-boxPadding*2-10);
+
+ String colourCodes = "";
+ Pattern pattern = Pattern.compile("\\u00A7.");
+ Matcher matcher = pattern.matcher(trimmed);
+ while(matcher.find()) {
+ colourCodes += matcher.group();
+ }
+
+ boolean firstLine = true;
+ int trimmedCharacters = trimmed.length();
+ while(true) {
+ if(trimmed.length() == line.length()) {
+ fr.drawString(trimmed, leftSide+boxPadding + 5, boxPadding + 10 + yOff, Color.WHITE.getRGB());
+ break;
+ } else if(trimmed.isEmpty()) {
+ yOff -= 12;
+ break;
+ } else {
+ if(firstLine) {
+ fr.drawString(trimmed, leftSide+boxPadding + 5, boxPadding + 10 + yOff, Color.WHITE.getRGB());
+ firstLine = false;
+ } else {
+ if(trimmed.startsWith(" ")) {
+ trimmed = trimmed.substring(1);
+ }
+ fr.drawString(colourCodes + trimmed, leftSide+boxPadding + 5, boxPadding + 10 + yOff, Color.WHITE.getRGB());
+ }
+
+ excess = line.substring(trimmedCharacters);
+ trimmed = trimToWidth(excess, width * 1 / 3 - boxPadding * 2 - 10);
+ trimmedCharacters += trimmed.length();
+ yOff += 12;
+ }
+ }
+
+ yOff += 16;
+ }
+ }
+ }
+
+ public float getInfoPaneOffsetFactor() {
+ if(itemPaneOffsetFactor.getValue() == 2/3f) {
+ return infoPaneOffsetFactor.getValue();
+ } else {
+ return Math.min(infoPaneOffsetFactor.getValue(), 1-itemPaneOffsetFactor.getValue());
+ }
+ }
+
+ public String trimToWidth(String str, int len) {
+ FontRenderer fr = Minecraft.getMinecraft().fontRendererObj;
+ String trim = fr.trimStringToWidth(str, len);
+
+ if(str.length() != trim.length() && !trim.endsWith(" ")) {
+ char next = str.charAt(trim.length());
+ if(next != ' ') {
+ String[] split = trim.split(" ");
+ String last = split[split.length-1];
+ if(last.length() < 8) {
+ trim = trim.substring(0, trim.length()-last.length());
+ }
+ }
+ }
+
+ return trim;
+ }
+
+ private void drawItemStack(ItemStack stack, int x, int y, String altText) {
+ RenderItem itemRender = Minecraft.getMinecraft().getRenderItem();
+ FontRenderer font = Minecraft.getMinecraft().fontRendererObj;
+
+ RenderHelper.enableGUIStandardItemLighting();
+ itemRender.renderItemAndEffectIntoGUI(stack, x, y);
+ RenderHelper.disableStandardItemLighting();
+
+ itemRender.renderItemOverlayIntoGUI(font, stack, x, y, altText);
+ }
+
+ private void drawTexturedRect(int x, int y, int width, int height) {
+ drawTexturedRect(x, y, width, height, 0, 1, 0 , 1);
+ }
+
+ private void drawTexturedRect(int x, int y, int width, int height, float uMin, float uMax, float vMin, float vMax) {
+ Tessellator tessellator = Tessellator.getInstance();
+ WorldRenderer worldrenderer = tessellator.getWorldRenderer();
+
+ GlStateManager.enableAlpha();
+ GlStateManager.enableBlend();
+ GlStateManager.blendFunc(770, 771);
+ GlStateManager.alphaFunc(516, 0.003921569F);
+
+ worldrenderer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
+ worldrenderer.pos(x + 0, y + height, this.zLevel).tex(uMin, vMax).endVertex();
+ worldrenderer.pos(x + width, y + height, this.zLevel).tex(uMax, vMax).endVertex();
+ worldrenderer.pos(x + width, y + 0, this.zLevel).tex(uMax, vMin).endVertex();
+ worldrenderer.pos(x + 0, y + 0, this.zLevel).tex(uMin, vMin).endVertex();
+ tessellator.draw();
+
+ GlStateManager.disableBlend();
+ }
+} \ No newline at end of file
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/NotEnoughUpdates.java b/src/main/java/io/github/moulberry/notenoughupdates/NotEnoughUpdates.java
new file mode 100644
index 00000000..17295aa8
--- /dev/null
+++ b/src/main/java/io/github/moulberry/notenoughupdates/NotEnoughUpdates.java
@@ -0,0 +1,159 @@
+package io.github.moulberry.notenoughupdates;
+
+import com.mojang.authlib.Agent;
+import com.mojang.authlib.GameProfile;
+import com.mojang.authlib.exceptions.AuthenticationException;
+import com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService;
+import com.mojang.authlib.yggdrasil.YggdrasilUserAuthentication;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.inventory.GuiContainer;
+import net.minecraft.util.EnumChatFormatting;
+import net.minecraft.util.Session;
+import net.minecraftforge.client.event.GuiOpenEvent;
+import net.minecraftforge.client.event.GuiScreenEvent;
+import net.minecraftforge.common.MinecraftForge;
+import net.minecraftforge.event.entity.player.ItemTooltipEvent;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.Mod.EventHandler;
+import net.minecraftforge.fml.common.event.FMLInitializationEvent;
+import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+import org.lwjgl.input.Keyboard;
+
+import javax.swing.*;
+import java.io.File;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.net.Proxy;
+import java.util.Scanner;
+import java.util.UUID;
+
+@Mod(modid = NotEnoughUpdates.MODID, version = NotEnoughUpdates.VERSION)
+public class NotEnoughUpdates {
+ public static final String MODID = "notenoughupdates";
+ public static final String VERSION = "1.0.0";
+
+ private NEUManager manager;
+ private NEUOverlay overlay;
+ private NEUIO neuio;
+
+ @EventHandler
+ public void preinit(FMLPreInitializationEvent event) {
+ MinecraftForge.EVENT_BUS.register(this);
+
+ File f = new File(event.getModConfigurationDirectory(), "notenoughupdates");
+ f.mkdirs();
+ //Github Access Token, may change. Value hard-coded.
+ neuio = new NEUIO("c49fcad378ae46e5c08bf6b0c5502f7e4830bfef");
+ manager = new NEUManager(neuio, f);
+ manager.loadItemInformation();
+ overlay = new NEUOverlay(manager);
+
+ //TODO: login code. Ignore this, used for testing.
+ /*try {
+ Field field = Minecraft.class.getDeclaredField("session");
+ YggdrasilUserAuthentication auth = (YggdrasilUserAuthentication)
+ new YggdrasilAuthenticationService(Proxy.NO_PROXY, UUID.randomUUID().toString())
+ .createUserAuthentication(Agent.MINECRAFT);
+ auth.setUsername("...");
+ JPasswordField pf = new JPasswordField();
+ JOptionPane.showConfirmDialog(null,
+ pf,
+ "Enter password:",
+ JOptionPane.NO_OPTION,
+ JOptionPane.PLAIN_MESSAGE);
+ auth.setPassword(new String(pf.getPassword()));
+ System.out.print("Attempting login...");
+
+ auth.logIn();
+
+ Session session = new Session(auth.getSelectedProfile().getName(),
+ auth.getSelectedProfile().getId().toString().replace("-", ""),
+ auth.getAuthenticatedToken(),
+ auth.getUserType().getName());
+
+ Field modifiersField = Field.class.getDeclaredField("modifiers");
+ modifiersField.setAccessible(true);
+ modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
+
+ field.setAccessible(true);
+ field.set(Minecraft.getMinecraft(), session);
+ } catch (NoSuchFieldException | AuthenticationException | IllegalAccessException e) {
+ e.printStackTrace();
+ }*/
+
+ }
+
+ @SubscribeEvent
+ public void onGuiOpen(GuiOpenEvent event) {
+ if(Minecraft.getMinecraft().currentScreen == null
+ && event.gui instanceof GuiContainer) {
+ overlay.reset();
+ }
+ }
+
+ @SubscribeEvent
+ public void onGuiScreenDraw(GuiScreenEvent.DrawScreenEvent.Post event) {
+ if(event.gui instanceof GuiContainer) {
+ overlay.render(event.renderPartialTicks, event.mouseX, event.mouseY);
+ }
+ }
+
+ @SubscribeEvent
+ public void onGuiScreenMouse(GuiScreenEvent.MouseInputEvent.Pre event) {
+ if(event.gui instanceof GuiContainer) {
+ if(overlay.mouseInput()) {
+ event.setCanceled(true);
+ }
+ }
+ }
+
+ @SubscribeEvent
+ public void onGuiScreenKeyboard(GuiScreenEvent.KeyboardInputEvent.Pre event) {
+ if(event.gui instanceof GuiContainer) {
+ if(overlay.keyboardInput()) {
+ event.setCanceled(true);
+ }
+ }
+ }
+
+ /**
+ * This was code leftover from testing but it ended up in the final mod so I guess its staying here.
+ * This makes it so that holding LCONTROL while hovering over an item with NBT will show the NBT of the item.
+ * Should probably have this disabled by default via config.
+ * @param event
+ */
+ @SubscribeEvent
+ public void onItemTooltip(ItemTooltipEvent event) {
+ if(!Keyboard.isKeyDown(Keyboard.KEY_LCONTROL)) return;
+ if(event.toolTip.get(event.toolTip.size()-1).startsWith(EnumChatFormatting.DARK_GRAY + "NBT: ")) {
+ event.toolTip.remove(event.toolTip.size()-1);
+
+ StringBuilder sb = new StringBuilder();
+ String nbt = event.itemStack.getTagCompound().toString();
+ int indent = 0;
+ for(char c : nbt.toCharArray()) {
+ boolean newline = false;
+ if(c == '{' || c == '[') {
+ indent++;
+ newline = true;
+ } else if(c == '}' || c == ']') {
+ indent--;
+ sb.append("\n");
+ for(int i=0; i<indent; i++) sb.append(" ");
+ } else if(c == ',') {
+ newline = true;
+ } else if(c == '\"') {
+ sb.append(EnumChatFormatting.RESET.toString() + EnumChatFormatting.GRAY);
+ }
+
+ sb.append(c);
+ if(newline) {
+ sb.append("\n");
+ for(int i=0; i<indent; i++) sb.append(" ");
+ }
+ }
+ event.toolTip.add(sb.toString());
+ }
+ }
+}
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/itemeditor/GuiElement.java b/src/main/java/io/github/moulberry/notenoughupdates/itemeditor/GuiElement.java
new file mode 100644
index 00000000..06a8810d
--- /dev/null
+++ b/src/main/java/io/github/moulberry/notenoughupdates/itemeditor/GuiElement.java
@@ -0,0 +1,15 @@
+package io.github.moulberry.notenoughupdates.itemeditor;
+
+import net.minecraft.client.gui.Gui;
+
+public abstract class GuiElement extends Gui {
+
+ public abstract void render(int x, int y);
+ public abstract int getWidth();
+ public abstract int getHeight();
+ public void mouseClicked(int mouseX, int mouseY, int mouseButton) {}
+ public void mouseClickMove(int mouseX, int mouseY, int clickedMouseButton, long timeSinceLastClick) {}
+ public void otherComponentClick() {}
+ public void keyTyped(char typedChar, int keyCode) {}
+
+}
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/itemeditor/GuiElementButton.java b/src/main/java/io/github/moulberry/notenoughupdates/itemeditor/GuiElementButton.java
new file mode 100644
index 00000000..0ed03c36
--- /dev/null
+++ b/src/main/java/io/github/moulberry/notenoughupdates/itemeditor/GuiElementButton.java
@@ -0,0 +1,35 @@
+package io.github.moulberry.notenoughupdates.itemeditor;
+
+import java.awt.*;
+
+public class GuiElementButton extends GuiElementText {
+
+ private Runnable callback;
+
+ public GuiElementButton(String text, int colour, Runnable callback) {
+ super(text, colour);
+ this.callback = callback;
+ }
+
+ @Override
+ public int getHeight() {
+ return super.getHeight() + 5;
+ }
+
+ @Override
+ public int getWidth() {
+ return super.getWidth() + 10;
+ }
+
+ @Override
+ public void mouseClicked(int mouseX, int mouseY, int mouseButton) {
+ callback.run();
+ }
+
+ @Override
+ public void render(int x, int y) {
+ drawRect(x, y, x+getWidth(), y+super.getHeight(), Color.WHITE.getRGB());
+ drawRect(x+1, y+1, x+getWidth()-1, y+super.getHeight()-1, Color.BLACK.getRGB());
+ super.render(x+5, y-1);
+ }
+}
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/itemeditor/GuiElementText.java b/src/main/java/io/github/moulberry/notenoughupdates/itemeditor/GuiElementText.java
new file mode 100644
index 00000000..28bc9b71
--- /dev/null
+++ b/src/main/java/io/github/moulberry/notenoughupdates/itemeditor/GuiElementText.java
@@ -0,0 +1,42 @@
+package io.github.moulberry.notenoughupdates.itemeditor;
+
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.FontRenderer;
+
+public class GuiElementText extends GuiElement {
+
+ protected String text;
+ private int colour;
+
+ public GuiElementText(String text, int colour) {
+ this.text = text;
+ this.colour = colour;
+ }
+
+ @Override
+ public int getHeight() {
+ return 18;
+ }
+
+ @Override
+ public int getWidth() {
+ FontRenderer fr = Minecraft.getMinecraft().fontRendererObj;
+ return fr.getStringWidth(text);
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public void setText(String text) {
+ this.text = text;
+ }
+
+ @Override
+ public void render(int x, int y) {
+ FontRenderer fr = Minecraft.getMinecraft().fontRendererObj;
+
+ fr.drawString(text, x, y+6, colour);
+ }
+
+}
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/itemeditor/GuiElementTextField.java b/src/main/java/io/github/moulberry/notenoughupdates/itemeditor/GuiElementTextField.java
new file mode 100644
index 00000000..5ad5c66c
--- /dev/null
+++ b/src/main/java/io/github/moulberry/notenoughupdates/itemeditor/GuiElementTextField.java
@@ -0,0 +1,407 @@
+package io.github.moulberry.notenoughupdates.itemeditor;
+
+import com.google.common.base.Predicate;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.GuiScreen;
+import net.minecraft.client.gui.GuiTextField;
+import net.minecraft.client.gui.ScaledResolution;
+import net.minecraft.client.renderer.GlStateManager;
+import org.apache.commons.lang3.StringUtils;
+
+import javax.annotation.Nullable;
+import java.awt.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class GuiElementTextField extends GuiElement {
+
+ public static final int NUM_ONLY = 0b10000;
+ public static final int NO_SPACE = 0b01000;
+ public static final int FORCE_CAPS = 0b00100;
+ public static final int COLOUR = 0b00010;
+ public static final int MULTILINE = 0b00001;
+
+ private static final int searchBarYSize = 20;
+ private static final int searchBarXSize = 350;
+ private static final int searchBarPadding = 2;
+
+ private int options = 0;
+
+ private boolean focus = false;
+
+ private int x;
+ private int y;
+
+ private GuiTextField textField = new GuiTextField(0, Minecraft.getMinecraft().fontRendererObj,
+ 0 , 0, 0, 0);
+
+ public GuiElementTextField(String initialText, int options) {
+ textField.setFocused(true);
+ textField.setCanLoseFocus(false);
+ textField.setMaxStringLength(999);
+ textField.setText(initialText);
+ this.options = options;
+ }
+
+ @Override
+ public String toString() {
+ return textField.getText();
+ }
+
+ @Override
+ public int getHeight() {
+ ScaledResolution scaledresolution = new ScaledResolution(Minecraft.getMinecraft());
+ int paddingUnscaled = searchBarPadding/scaledresolution.getScaleFactor();
+
+ int numLines = StringUtils.countMatches(textField.getText(), "\n")+1;
+ int extraSize = (searchBarYSize-8)/2+8;
+ int bottomTextBox = searchBarYSize + extraSize*(numLines-1);
+
+ return bottomTextBox + paddingUnscaled*2;
+ }
+
+ @Override
+ public int getWidth() {
+ ScaledResolution scaledresolution = new ScaledResolution(Minecraft.getMinecraft());
+ int paddingUnscaled = searchBarPadding/scaledresolution.getScaleFactor();
+
+ return searchBarXSize + paddingUnscaled*2;
+ }
+
+ public int getCursorPos(int mouseX, int mouseY) {
+ int xComp = mouseX - x;
+ int yComp = mouseY - y;
+
+ int extraSize = (searchBarYSize-8)/2+8;
+
+ int lineNum = Math.round(((yComp - (searchBarYSize-8)/2))/extraSize);
+
+ Pattern patternControlCode = Pattern.compile("(?i)\\u00A7([^\\u00B6])(?!\\u00B6)");
+ String text = textField.getText();
+ String textNoColour = textField.getText();
+ while(true) {
+ Matcher matcher = patternControlCode.matcher(text);
+ if(!matcher.find() || matcher.groupCount() < 1) break;
+ String code = matcher.group(1);
+ text = matcher.replaceFirst("\u00A7"+code+"\u00B6"+code);
+ }
+ while(true) {
+ Matcher matcher = patternControlCode.matcher(textNoColour);
+ if(!matcher.find() || matcher.groupCount() < 1) break;
+ String code = matcher.group(1);
+ textNoColour = matcher.replaceFirst("\u00B6"+code);
+ }
+
+ int currentLine = 0;
+ int cursorIndex = 0;
+ for(; cursorIndex<textNoColour.length(); cursorIndex++) {
+ if(currentLine == lineNum) break;
+ if(textNoColour.charAt(cursorIndex) == '\n') {
+ currentLine++;
+ }
+ }
+ String textNC = textNoColour.substring(0, cursorIndex);
+ int colorCodes = StringUtils.countMatches(textNC, "\u00B6");
+ String line = text.substring(cursorIndex+colorCodes*2).split("\n")[0];
+ String trimmed = Minecraft.getMinecraft().fontRendererObj.trimStringToWidth(line, xComp-5);
+ int linePos = strLenNoColor(trimmed);
+ if(linePos != strLenNoColor(line)) {
+ char after = line.charAt(linePos);
+ int trimmedWidth = Minecraft.getMinecraft().fontRendererObj.getStringWidth(trimmed);
+ int charWidth = Minecraft.getMinecraft().fontRendererObj.getCharWidth(after);
+ if(trimmedWidth + charWidth/2 < xComp-5) {
+ linePos++;
+ }
+ }
+ cursorIndex += linePos;
+ return cursorIndex;
+ }
+
+ @Override
+ public void mouseClicked(int mouseX, int mouseY, int mouseButton) {
+ if(mouseButton == 1) {
+ textField.setText("");
+ } else {
+ textField.setCursorPosition(getCursorPos(mouseX, mouseY));
+ }
+ focus = true;
+ }
+
+ public void otherComponentClick() {
+ focus = false;
+ textField.setSelectionPos(textField.getCursorPosition());
+ }
+
+ public int strLenNoColor(String str) {
+ return str.replaceAll("(?i)\\u00A7.", "").length();
+ }
+
+ public void mouseClickMove(int mouseX, int mouseY, int clickedMouseButton, long timeSinceLastClick) {
+ if(focus) {
+ textField.setSelectionPos(getCursorPos(mouseX, mouseY));
+ }
+ }
+
+ @Override
+ public void keyTyped(char typedChar, int keyCode) {
+ if(focus) {
+ if((options & MULTILINE) != 0) { //Carriage return
+ Pattern patternControlCode = Pattern.compile("(?i)\\u00A7([^\\u00B6\n])(?!\\u00B6)");
+
+ String text = textField.getText();
+ String textNoColour = textField.getText();
+ while(true) {
+ Matcher matcher = patternControlCode.matcher(text);
+ if(!matcher.find() || matcher.groupCount() < 1) break;
+ String code = matcher.group(1);
+ text = matcher.replaceFirst("\u00A7"+code+"\u00B6"+code);
+ }
+ while(true) {
+ Matcher matcher = patternControlCode.matcher(textNoColour);
+ if(!matcher.find() || matcher.groupCount() < 1) break;
+ String code = matcher.group(1);
+ textNoColour = matcher.replaceFirst("\u00B6"+code);
+ }
+
+ if(keyCode == 28) {
+ String before = textField.getText().substring(0, textField.getCursorPosition());
+ String after = textField.getText().substring(textField.getCursorPosition());
+ int pos = textField.getCursorPosition();
+ textField.setText(before + "\n" + after);
+ textField.setCursorPosition(pos+1);
+ return;
+ } else if(keyCode == 200) { //Up
+ String textNCBeforeCursor = textNoColour.substring(0, textField.getSelectionEnd());
+ int colorCodes = StringUtils.countMatches(textNCBeforeCursor, "\u00B6");
+ String textBeforeCursor = text.substring(0, textField.getSelectionEnd()+colorCodes*2);
+
+ int numLinesBeforeCursor = StringUtils.countMatches(textBeforeCursor, "\n");
+
+ String[] split = textBeforeCursor.split("\n");
+ int textBeforeCursorWidth;
+ String lineBefore;
+ String thisLineBeforeCursor;
+ if(split.length == numLinesBeforeCursor && split.length > 0) {
+ textBeforeCursorWidth = 0;
+ lineBefore = split[split.length-1];
+ thisLineBeforeCursor = "";
+ } else if(split.length > 1) {
+ thisLineBeforeCursor = split[split.length-1];
+ lineBefore = split[split.length-2];
+ textBeforeCursorWidth = Minecraft.getMinecraft().fontRendererObj.getStringWidth(thisLineBeforeCursor);
+ } else {
+ return;
+ }
+ String trimmed = Minecraft.getMinecraft().fontRendererObj
+ .trimStringToWidth(lineBefore, textBeforeCursorWidth);
+ int linePos = strLenNoColor(trimmed);
+ if(linePos != strLenNoColor(lineBefore)) {
+ char after = lineBefore.charAt(linePos);
+ int trimmedWidth = Minecraft.getMinecraft().fontRendererObj.getStringWidth(trimmed);
+ int charWidth = Minecraft.getMinecraft().fontRendererObj.getCharWidth(after);
+ if(trimmedWidth + charWidth/2 < textBeforeCursorWidth) {
+ linePos++;
+ }
+ }
+ int newPos = textField.getSelectionEnd()-strLenNoColor(thisLineBeforeCursor)
+ -strLenNoColor(lineBefore)-1+linePos;
+
+ if(GuiScreen.isShiftKeyDown()) {
+ textField.setSelectionPos(newPos);
+ } else {
+ textField.setCursorPosition(newPos);
+ }
+ } else if(keyCode == 208) { //Down
+ String textNCBeforeCursor = textNoColour.substring(0, textField.getSelectionEnd());
+ int colorCodes = StringUtils.countMatches(textNCBeforeCursor, "\u00B6");
+ String textBeforeCursor = text.substring(0, textField.getSelectionEnd()+colorCodes*2);
+
+ int numLinesBeforeCursor = StringUtils.countMatches(textBeforeCursor, "\n");
+
+ String[] split = textBeforeCursor.split("\n");
+ String thisLineBeforeCursor;
+ int textBeforeCursorWidth;
+ if(split.length == numLinesBeforeCursor) {
+ thisLineBeforeCursor = "";
+ textBeforeCursorWidth = 0;
+ } else if(split.length > 0) {
+ thisLineBeforeCursor = split[split.length-1];
+ textBeforeCursorWidth = Minecraft.getMinecraft().fontRendererObj.getStringWidth(thisLineBeforeCursor);
+ } else {
+ return;
+ }
+
+ String[] split2 = textNoColour.split("\n");
+ if(split2.length > numLinesBeforeCursor+1) {
+ String lineAfter = split2[numLinesBeforeCursor+1];
+ String trimmed = Minecraft.getMinecraft().fontRendererObj
+ .trimStringToWidth(lineAfter, textBeforeCursorWidth);
+ int linePos = strLenNoColor(trimmed);
+ if(linePos != strLenNoColor(lineAfter)) {
+ char after = lineAfter.charAt(linePos);
+ int trimmedWidth = Minecraft.getMinecraft().fontRendererObj.getStringWidth(trimmed);
+ int charWidth = Minecraft.getMinecraft().fontRendererObj.getCharWidth(after);
+ if(trimmedWidth + charWidth/2 < textBeforeCursorWidth) {
+ linePos++;
+ }
+ }
+ int newPos = textField.getSelectionEnd()-strLenNoColor(thisLineBeforeCursor)
+ +strLenNoColor(split2[numLinesBeforeCursor])+1+linePos;
+
+ if(GuiScreen.isShiftKeyDown()) {
+ textField.setSelectionPos(newPos);
+ } else {
+ textField.setCursorPosition(newPos);
+ }
+ }
+ }
+ }
+
+ String old = textField.getText();
+ if((options & FORCE_CAPS) != 0) typedChar = Character.toUpperCase(typedChar);
+ if((options & NO_SPACE) != 0 && typedChar == ' ') return;
+
+ textField.textboxKeyTyped(typedChar, keyCode);
+
+ if((options & COLOUR) != 0) {
+ if(typedChar == '&') {
+ int pos = textField.getCursorPosition()-2;
+ if(pos >= 0 && pos < textField.getText().length()) {
+ if(textField.getText().charAt(pos) == '&') {
+ String before = textField.getText().substring(0, pos);
+ String after = "";
+ if(pos+2 < textField.getText().length()) {
+ after = textField.getText().substring(pos+2);
+ }
+ textField.setText(before + "\u00A7" + after);
+ textField.setCursorPosition(pos+1);
+ }
+ }
+
+ }
+ }
+
+ if((options & NUM_ONLY) != 0 && textField.getText().matches("[^0-9]")) textField.setText(old);
+ }
+ }
+
+ public void render(int x, int y) {
+ this.x = x;
+ this.y = y;
+ drawTextbox(x, y, searchBarXSize, searchBarYSize, searchBarPadding, textField, focus);
+ }
+
+ private void drawTextbox(int x, int y, int searchBarXSize, int searchBarYSize, int searchBarPadding,
+ GuiTextField textField, boolean focus) {
+ ScaledResolution scaledresolution = new ScaledResolution(Minecraft.getMinecraft());
+
+ GlStateManager.disableLighting();
+
+ /**
+ * Search bar
+ */
+ int paddingUnscaled = searchBarPadding/scaledresolution.getScaleFactor();
+ if(paddingUnscaled < 1) paddingUnscaled = 1;
+
+ int numLines = StringUtils.countMatches(textField.getText(), "\n")+1;
+ int extraSize = (searchBarYSize-8)/2+8;
+ int bottomTextBox = y + searchBarYSize + extraSize*(numLines-1);
+
+ //Search bar background
+ drawRect(x - paddingUnscaled,
+ y - paddingUnscaled,
+ x + searchBarXSize + paddingUnscaled,
+ bottomTextBox + paddingUnscaled, focus ? Color.GREEN.getRGB() : Color.WHITE.getRGB());
+ drawRect(x,
+ y,
+ x + searchBarXSize,
+ bottomTextBox, Color.BLACK.getRGB());
+
+ //Search bar text
+ Pattern patternControlCode = Pattern.compile("(?i)\\u00A7([^\\u00B6\n])(?!\\u00B6)");
+
+ String text = textField.getText();
+ String textNoColor = textField.getText();
+ while(true) {
+ Matcher matcher = patternControlCode.matcher(text);
+ if(!matcher.find() || matcher.groupCount() < 1) break;
+ String code = matcher.group(1);
+ text = matcher.replaceFirst("\u00A7"+code+"\u00B6"+code);
+ }
+ while(true) {
+ Matcher matcher = patternControlCode.matcher(textNoColor);
+ if(!matcher.find() || matcher.groupCount() < 1) break;
+ String code = matcher.group(1);
+ textNoColor = matcher.replaceFirst("\u00B6"+code);
+ }
+
+ String[] texts = text.split("\n");
+ for(int yOffI = 0; yOffI < texts.length; yOffI++) {
+ int yOff = yOffI*extraSize;
+
+ Minecraft.getMinecraft().fontRendererObj.drawString(texts[yOffI], x + 5,
+ y+(searchBarYSize-8)/2+yOff, Color.WHITE.getRGB());
+ }
+
+ if(focus && System.currentTimeMillis()%1000>500) {
+ String textNCBeforeCursor = textNoColor.substring(0, textField.getCursorPosition());
+ int colorCodes = StringUtils.countMatches(textNCBeforeCursor, "\u00B6");
+ String textBeforeCursor = text.substring(0, textField.getCursorPosition()+colorCodes*2);
+
+ int numLinesBeforeCursor = StringUtils.countMatches(textBeforeCursor, "\n");
+ int yOff = numLinesBeforeCursor*extraSize;
+
+ String[] split = textBeforeCursor.split("\n");
+ int textBeforeCursorWidth;
+ if(split.length <= numLinesBeforeCursor || split.length == 0) {
+ textBeforeCursorWidth = 0;
+ } else {
+ textBeforeCursorWidth = Minecraft.getMinecraft().fontRendererObj.getStringWidth(split[split.length-1]);
+ }
+ drawRect(x + 5 + textBeforeCursorWidth,
+ y+(searchBarYSize-8)/2-1 + yOff,
+ x + 5 + textBeforeCursorWidth+1,
+ y+(searchBarYSize-8)/2+9 + yOff, Color.WHITE.getRGB());
+ }
+
+ String selectedText = textField.getSelectedText();
+ if(!selectedText.isEmpty()) {
+ int leftIndex = textField.getCursorPosition() < textField.getSelectionEnd() ?
+ textField.getCursorPosition() : textField.getSelectionEnd();
+ int rightIndex = textField.getCursorPosition() > textField.getSelectionEnd() ?
+ textField.getCursorPosition() : textField.getSelectionEnd();
+
+ int texX = 0;
+ int texY = 0;
+ for(int i=0; i<textNoColor.length(); i++) {
+ char c = textNoColor.charAt(i);
+ if(c == '\n') {
+ if(i >= leftIndex && i < rightIndex) {
+ drawRect(x + 5 + texX,
+ y+(searchBarYSize-8)/2-1 + texY,
+ x + 5 + texX + 3,
+ y+(searchBarYSize-8)/2+9 + texY, Color.LIGHT_GRAY.getRGB());
+ }
+
+ texX = 0;
+ texY += extraSize;
+ continue;
+ }
+
+ int len = Minecraft.getMinecraft().fontRendererObj.getStringWidth(String.valueOf(c));
+ if(i >= leftIndex && i < rightIndex) {
+ drawRect(x + 5 + texX,
+ y+(searchBarYSize-8)/2-1 + texY,
+ x + 5 + texX + len,
+ y+(searchBarYSize-8)/2+9 + texY, Color.LIGHT_GRAY.getRGB());
+
+ Minecraft.getMinecraft().fontRendererObj.drawString(String.valueOf(c),
+ x + 5 + texX,
+ y+(searchBarYSize-8)/2 + texY, Color.BLACK.getRGB());
+ }
+
+ texX += len;
+ }
+ }
+ }
+}
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/itemeditor/NEUItemEditor.java b/src/main/java/io/github/moulberry/notenoughupdates/itemeditor/NEUItemEditor.java
new file mode 100644
index 00000000..6cc8aacd
--- /dev/null
+++ b/src/main/java/io/github/moulberry/notenoughupdates/itemeditor/NEUItemEditor.java
@@ -0,0 +1,417 @@
+package io.github.moulberry.notenoughupdates.itemeditor;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import io.github.moulberry.notenoughupdates.LerpingInteger;
+import io.github.moulberry.notenoughupdates.NEUManager;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.*;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.client.renderer.RenderHelper;
+import net.minecraft.client.renderer.entity.RenderItem;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.*;
+import net.minecraft.util.ResourceLocation;
+import net.minecraftforge.fml.client.config.GuiUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.lwjgl.input.Keyboard;
+import org.lwjgl.input.Mouse;
+import org.lwjgl.opengl.GL11;
+
+import java.awt.*;
+import java.io.IOException;
+import java.util.*;
+import java.util.List;
+import java.util.function.Supplier;
+import static io.github.moulberry.notenoughupdates.itemeditor.GuiElementTextField.*;
+
+public class NEUItemEditor extends GuiScreen {
+
+ private NEUManager manager;
+
+ private List<GuiElement> options = new ArrayList<>();
+ private List<GuiElement> rightOptions = new ArrayList<>();
+
+ private JsonObject item;
+
+ private static final int PADDING = 10;
+ private static final int SCROLL_AMOUNT = 20;
+
+ private LerpingInteger scrollHeight = new LerpingInteger(0);
+
+ private Supplier<String> internalname;
+ private Supplier<String> itemid;
+ private Supplier<String> displayname;
+ private Supplier<String> lore;
+ private Supplier<String> info;
+ private Supplier<String> clickcommand;
+ private Supplier<String> damage;
+ private NBTTagCompound nbttag;
+
+ public NEUItemEditor(NEUManager manager, String internalname, JsonObject item) {
+ this.manager = manager;
+ this.item = item;
+
+ if(item.has("nbttag")) {
+ try {
+ nbttag = JsonToNBT.getTagFromJson(item.get("nbttag").getAsString());
+ } catch(NBTException e) {}
+ }
+
+ internalname = internalname == null ? "" : internalname;
+ options.add(new GuiElementText("Internal Name: ", Color.WHITE.getRGB()));
+ this.internalname = addTextFieldWithSupplier(internalname, NO_SPACE | FORCE_CAPS);
+
+ options.add(new GuiElementText("Item ID: ", Color.WHITE.getRGB()));
+ String itemid = item.has("itemid") ? item.get("itemid").getAsString() : "";
+ this.itemid = addTextFieldWithSupplier(itemid, NO_SPACE);
+
+ options.add(new GuiElementText("Display name: ", Color.WHITE.getRGB()));
+ String displayname = item.has("displayname") ? item.get("displayname").getAsString() : "";
+ this.displayname = addTextFieldWithSupplier(displayname, COLOUR);
+
+ options.add(new GuiElementText("Lore: ", Color.WHITE.getRGB()));
+ JsonArray lore = item.has("lore") ? item.get("lore").getAsJsonArray() : new JsonArray();
+ String[] loreA = new String[lore.size()];
+ for(int i=0; i<lore.size(); i++) loreA[i] = lore.get(i).getAsString();
+ this.lore = addTextFieldWithSupplier(String.join("\n", loreA), COLOUR | MULTILINE);
+
+ options.add(new GuiElementText("Additional information: ", Color.WHITE.getRGB()));
+ this.info = addTextFieldWithSupplier("", COLOUR | MULTILINE);
+
+ options.add(new GuiElementText("Click-command (viewrecipe or viewpotion): ", Color.WHITE.getRGB()));
+ String clickcommand = item.has("clickcommand") ? item.get("clickcommand").getAsString() : "";
+ this.clickcommand = addTextFieldWithSupplier(clickcommand, NO_SPACE);
+
+ options.add(new GuiElementText("Damage: ", Color.WHITE.getRGB()));
+ String damage = item.has("damage") ? item.get("damage").getAsString() : "";
+ this.damage = addTextFieldWithSupplier(damage, NO_SPACE | NUM_ONLY);
+
+ rightOptions.add(new GuiElementButton("Close (discards changes)", Color.LIGHT_GRAY.getRGB(), () -> {
+ Minecraft.getMinecraft().displayGuiScreen(null);
+ }));
+ GuiElementButton button = new Object() { //Used to make the compiler shut the fuck up
+ GuiElementButton b = new GuiElementButton("Save to local disk", Color.GREEN.getRGB(), new Runnable() {
+ public void run() {
+ if(save()) {
+ b.setText("Save to local disk (SUCCESS)");
+ } else {
+ b.setText("Save to local disk (FAILED)");
+ }
+ }
+ });
+ }.b;
+ rightOptions.add(button);
+
+ button = new Object() { //Used to make the compiler shut the fuck up
+ GuiElementButton b = new GuiElementButton("Upload", Color.YELLOW.getRGB(), new Runnable() {
+ public void run() {
+ if(b.getText().equals("Upload")) {
+ b.setText("Confirm upload?");
+ } else {
+ if(upload()) {
+ b.setText("Uploaded");
+ } else {
+ b.setText("Upload failed.");
+ }
+ }
+ }
+ });
+ }.b;
+ rightOptions.add(button);
+
+ rightOptions.add(new GuiElementText("", Color.WHITE.getRGB()));
+
+ rightOptions.add(new GuiElementButton("Remove enchants", Color.RED.getRGB(), () -> {
+ nbttag.removeTag("ench");
+ nbttag.getCompoundTag("ExtraAttributes").removeTag("enchantments");
+ }));
+ rightOptions.add(new GuiElementButton("Add enchant glint", Color.ORANGE.getRGB(), () -> {
+ nbttag.setTag("ench", new NBTTagList());
+ }));
+
+ resetScrollToTop();
+ }
+
+ public boolean save() {
+ int damageI = 0;
+ try {
+ damageI = Integer.valueOf(damage.get());
+ } catch(NumberFormatException e) {}
+ resyncNbttag();
+ String[] infoA = info.get().trim().split("\n");
+ if(infoA.length == 0 || infoA[0].isEmpty()) {
+ infoA = new String[0];
+ }
+ return manager.writeItemJson(internalname.get(), itemid.get(), displayname.get(), lore.get().split("\n"),
+ infoA, clickcommand.get(), damageI, nbttag);
+ }
+
+ public boolean upload() {
+ int damageI = 0;
+ try {
+ damageI = Integer.valueOf(damage.get());
+ } catch(NumberFormatException e) {}
+ resyncNbttag();
+ String[] infoA = info.get().trim().split("\n");
+ if(infoA.length == 0 || infoA[0].isEmpty()) {
+ infoA = new String[0];
+ }
+ return manager.uploadItemJson(internalname.get(), itemid.get(), displayname.get(), lore.get().split("\n"),
+ infoA, clickcommand.get(), damageI, nbttag);
+ }
+
+ public void onGuiClosed() {
+ Keyboard.enableRepeatEvents(false);
+ }
+
+ public Supplier<String> addTextFieldWithSupplier(String initialText, int options) {
+ GuiElementTextField textField = new GuiElementTextField(initialText, options);
+ this.options.add(textField);
+ return () -> textField.toString();
+ }
+
+ public void resyncNbttag() {
+ if(nbttag == null) nbttag = new NBTTagCompound();
+
+ //Item lore
+ NBTTagList list = new NBTTagList();
+ for(String lore : this.lore.get().split("\n")) {
+ list.appendTag(new NBTTagString(lore));
+ }
+
+ NBTTagCompound display = nbttag.getCompoundTag("display");
+ display.setTag("Lore", list);
+
+ //Name
+ display.setString("Name", displayname.get());
+ nbttag.setTag("display", display);
+
+ //Internal ID
+ NBTTagCompound ea = nbttag.getCompoundTag("ExtraAttributes");
+ ea.setString("id", internalname.get());
+ nbttag.setTag("ExtraAttributes", ea);
+ }
+
+ public void resetScrollToTop() {
+ int totalHeight = PADDING;
+ for(GuiElement gui : options) {
+ totalHeight += gui.getHeight();
+ }
+
+ ScaledResolution scaledresolution = new ScaledResolution(Minecraft.getMinecraft());
+ int height = scaledresolution.getScaledHeight();
+
+ scrollHeight.setValue(totalHeight-height+PADDING);
+ }
+
+ public int calculateYScroll() {
+ ScaledResolution scaledresolution = new ScaledResolution(Minecraft.getMinecraft());
+ int height = scaledresolution.getScaledHeight();
+
+ int totalHeight = PADDING;
+ for(GuiElement gui : options) {
+ totalHeight += gui.getHeight();
+ }
+
+ if(scrollHeight.getValue() < 0) scrollHeight.setValue(0);
+
+ int yScroll = 0;
+ if(totalHeight > height-PADDING) {
+ yScroll = totalHeight-height+PADDING-scrollHeight.getValue();
+ } else {
+ scrollHeight.setValue(0);
+ }
+ if(yScroll < 0) {
+ yScroll = 0;
+ scrollHeight.setValue(totalHeight-height+PADDING);
+ }
+
+ return yScroll;
+ }
+
+ @Override
+ public void drawScreen(int mouseX, int mouseY, float partialTicks) {
+ scrollHeight.tick();
+
+ ScaledResolution scaledresolution = new ScaledResolution(Minecraft.getMinecraft());
+ int width = scaledresolution.getScaledWidth();
+ int height = scaledresolution.getScaledHeight();
+
+ GlStateManager.disableLighting();
+
+ Color backgroundColour = new Color(10, 10, 10, 240);
+ drawRect(0, 0, width, height, backgroundColour.getRGB());
+
+ int yScroll = calculateYScroll();
+ if(yScroll > 0){
+ //Render scroll bar
+ }
+
+ int currentY = PADDING-yScroll;
+ for(GuiElement gui : options) {
+ gui.render(PADDING, currentY);
+ currentY += gui.getHeight();
+ }
+
+ currentY = PADDING;
+ for(GuiElement gui : rightOptions) {
+ gui.render(width-PADDING-gui.getWidth(), currentY);
+ currentY += gui.getHeight();
+ }
+
+ int itemX = 424;
+ int itemY = 32;
+ int itemSize = 128;
+ Color itemBorder = new Color(100, 50, 150, 255);
+ Color itemBackground = new Color(120, 120, 120, 255);
+ drawRect(itemX-10, itemY-10, itemX+itemSize+10, itemY+itemSize+10, Color.DARK_GRAY.getRGB());
+ drawRect(itemX-9, itemY-9, itemX+itemSize+9, itemY+itemSize+9, itemBorder.getRGB());
+ drawRect(itemX-6, itemY-6, itemX+itemSize+6, itemY+itemSize+6, Color.DARK_GRAY.getRGB());
+ drawRect(itemX-5, itemY-5, itemX+itemSize+5, itemY+itemSize+5, itemBackground.getRGB());
+ ItemStack stack = new ItemStack(Item.itemRegistry.getObject(new ResourceLocation(itemid.get())));
+
+ if(stack.getItem() != null) {
+ try {
+ stack.setItemDamage(Integer.valueOf(damage.get()));
+ } catch(NumberFormatException e) {}
+
+ resyncNbttag();
+ stack.setTagCompound(nbttag);
+
+ int scaleFactor = itemSize/16;
+ GL11.glPushMatrix();
+ GlStateManager.scale(scaleFactor, scaleFactor, 1);
+ drawItemStack(stack, itemX/scaleFactor, itemY/scaleFactor, null);
+ GL11.glPopMatrix();
+ }
+
+ //Tooltip
+ List<String> text = new ArrayList<>();
+ text.add(displayname.get());
+ text.addAll(Arrays.asList(lore.get().split("\n")));
+
+ GuiUtils.drawHoveringText(text, itemX-20, itemY+itemSize+28, width, height, -1,
+ Minecraft.getMinecraft().fontRendererObj);
+
+ GlStateManager.disableLighting();
+ }
+
+ @Override
+ protected void keyTyped(char typedChar, int keyCode) {
+ for(GuiElement gui : options) {
+ gui.keyTyped(typedChar, keyCode);
+ }
+ }
+
+ @Override
+ protected void mouseClickMove(int mouseX, int mouseY, int clickedMouseButton, long timeSinceLastClick) {
+ ScaledResolution scaledresolution = new ScaledResolution(Minecraft.getMinecraft());
+ int width = scaledresolution.getScaledWidth();
+
+ int yScroll = calculateYScroll();
+ int currentY = PADDING-yScroll;
+ for(GuiElement gui : options) {
+ if(mouseY > currentY && mouseY < currentY+gui.getHeight()
+ && mouseX < gui.getWidth()) {
+ gui.mouseClickMove(mouseX, mouseY, clickedMouseButton, timeSinceLastClick);
+ return;
+ }
+ currentY += gui.getHeight();
+ }
+
+ currentY = PADDING;
+ for(GuiElement gui : rightOptions) {
+ if(mouseY > currentY && mouseY < currentY+gui.getHeight()
+ && mouseX > width-PADDING-gui.getWidth()) {
+ gui.mouseClickMove(mouseX, mouseY, clickedMouseButton, timeSinceLastClick);
+ return;
+ }
+ currentY += gui.getHeight();
+ }
+ }
+
+ @Override
+ public void handleMouseInput() throws IOException {
+ ScaledResolution scaledresolution = new ScaledResolution(Minecraft.getMinecraft());
+
+ int maxWidth = 0;
+ for(GuiElement gui : options) {
+ if(gui.getWidth() > maxWidth) maxWidth = gui.getWidth();
+ }
+
+ if(Mouse.getX() < maxWidth*scaledresolution.getScaleFactor()) {
+ int dWheel = Mouse.getEventDWheel();
+
+ if(dWheel < 0) {
+ scrollHeight.setTarget(scrollHeight.getTarget()-SCROLL_AMOUNT);
+ scrollHeight.resetTimer();
+ } else if(dWheel > 0) {
+ scrollHeight.setTarget(scrollHeight.getTarget()+SCROLL_AMOUNT);
+ scrollHeight.resetTimer();
+ }
+ }
+
+ super.handleMouseInput();
+ }
+
+ @Override
+ protected void mouseClicked(int mouseX, int mouseY, int mouseButton) {
+ ScaledResolution scaledresolution = new ScaledResolution(Minecraft.getMinecraft());
+ int width = scaledresolution.getScaledWidth();
+
+ int yScroll = calculateYScroll();
+ int currentY = PADDING-yScroll;
+ for(GuiElement gui : options) {
+ if(mouseY > currentY && mouseY < currentY+gui.getHeight()
+ && mouseX < gui.getWidth()) {
+ gui.mouseClicked(mouseX, mouseY, mouseButton);
+ for(GuiElement gui2 : options) {
+ if(gui2 != gui) {
+ gui2.otherComponentClick();
+ }
+ }
+ for(GuiElement gui2 : rightOptions) {
+ if(gui2 != gui) {
+ gui2.otherComponentClick();
+ }
+ }
+ return;
+ }
+ currentY += gui.getHeight();
+ }
+
+ currentY = PADDING;
+ for(GuiElement gui : rightOptions) {
+ if(mouseY > currentY && mouseY < currentY+gui.getHeight()
+ && mouseX > width-PADDING-gui.getWidth()) {
+ gui.mouseClicked(mouseX, mouseY, mouseButton);
+ for(GuiElement gui2 : options) {
+ if(gui2 != gui) {
+ gui2.otherComponentClick();
+ }
+ }
+ for(GuiElement gui2 : rightOptions) {
+ if(gui2 != gui) {
+ gui2.otherComponentClick();
+ }
+ }
+ return;
+ }
+ currentY += gui.getHeight();
+ }
+ }
+
+ private void drawItemStack(ItemStack stack, int x, int y, String altText) {
+ RenderItem itemRender = Minecraft.getMinecraft().getRenderItem();
+ FontRenderer font = Minecraft.getMinecraft().fontRendererObj;
+
+ RenderHelper.enableGUIStandardItemLighting();
+ itemRender.renderItemAndEffectIntoGUI(stack, x, y);
+ RenderHelper.disableStandardItemLighting();
+
+ itemRender.renderItemOverlayIntoGUI(font, stack, x, y, altText);
+ }
+
+}
diff --git a/src/main/resources/assets/notenoughupdates/item_edit.png b/src/main/resources/assets/notenoughupdates/item_edit.png
new file mode 100644
index 00000000..6ab71631
--- /dev/null
+++ b/src/main/resources/assets/notenoughupdates/item_edit.png
Binary files differ
diff --git a/src/main/resources/assets/notenoughupdates/item_pane_tab_arrow.png b/src/main/resources/assets/notenoughupdates/item_pane_tab_arrow.png
new file mode 100644
index 00000000..d8fc8530
--- /dev/null
+++ b/src/main/resources/assets/notenoughupdates/item_pane_tab_arrow.png
Binary files differ
diff --git a/src/main/resources/assets/notenoughupdates/next.png b/src/main/resources/assets/notenoughupdates/next.png
new file mode 100644
index 00000000..8e4a96cc
--- /dev/null
+++ b/src/main/resources/assets/notenoughupdates/next.png
Binary files differ
diff --git a/src/main/resources/assets/notenoughupdates/next_pow2.png b/src/main/resources/assets/notenoughupdates/next_pow2.png
new file mode 100644
index 00000000..03365e1c
--- /dev/null
+++ b/src/main/resources/assets/notenoughupdates/next_pow2.png
Binary files differ
diff --git a/src/main/resources/assets/notenoughupdates/prev.png b/src/main/resources/assets/notenoughupdates/prev.png
new file mode 100644
index 00000000..a4cef66d
--- /dev/null
+++ b/src/main/resources/assets/notenoughupdates/prev.png
Binary files differ
diff --git a/src/main/resources/assets/notenoughupdates/prev_pow2.png b/src/main/resources/assets/notenoughupdates/prev_pow2.png
new file mode 100644
index 00000000..3b756716
--- /dev/null
+++ b/src/main/resources/assets/notenoughupdates/prev_pow2.png
Binary files differ
diff --git a/src/main/resources/mcmod.info b/src/main/resources/mcmod.info
new file mode 100644
index 00000000..d8192fc8
--- /dev/null
+++ b/src/main/resources/mcmod.info
@@ -0,0 +1,16 @@
+[
+{
+ "modid": "notenoughupdates",
+ "name": "NotEnoughUpdates",
+ "description": "Mod that shows recipes and useful info for Hypixel's Skyblock gamemode.",
+ "version": "${version}",
+ "mcversion": "${mcversion}",
+ "url": "",
+ "updateUrl": "",
+ "authorList": ["Moulberry"],
+ "credits": "Nullzee for listening to me rant. Contributors, etc. <3",
+ "logoFile": "",
+ "screenshots": [],
+ "dependencies": []
+}
+]