diff --git a/src/main/java/com/hbm/handler/HbmKeybinds.java b/src/main/java/com/hbm/handler/HbmKeybinds.java
index ad5b4d837..552ae9b6e 100644
--- a/src/main/java/com/hbm/handler/HbmKeybinds.java
+++ b/src/main/java/com/hbm/handler/HbmKeybinds.java
@@ -1,5 +1,7 @@
package com.hbm.handler;
+import com.hbm.inventory.gui.GUICalculator;
+import cpw.mods.fml.common.FMLCommonHandler;
import org.lwjgl.input.Keyboard;
import com.hbm.extprop.HbmPlayerProps;
@@ -16,6 +18,7 @@ public class HbmKeybinds {
public static final String category = "hbm.key";
+ public static KeyBinding calculatorKey = new KeyBinding(category + ".calculator", Keyboard.KEY_N, category);
public static KeyBinding jetpackKey = new KeyBinding(category + ".toggleBack", Keyboard.KEY_C, category);
public static KeyBinding hudKey = new KeyBinding(category + ".toggleHUD", Keyboard.KEY_V, category);
public static KeyBinding reloadKey = new KeyBinding(category + ".reload", Keyboard.KEY_R, category);
@@ -25,12 +28,13 @@ public class HbmKeybinds {
public static KeyBinding craneLeftKey = new KeyBinding(category + ".craneMoveLeft", Keyboard.KEY_LEFT, category);
public static KeyBinding craneRightKey = new KeyBinding(category + ".craneMoveRight", Keyboard.KEY_RIGHT, category);
public static KeyBinding craneLoadKey = new KeyBinding(category + ".craneLoad", Keyboard.KEY_RETURN, category);
-
+
public static void register() {
+ ClientRegistry.registerKeyBinding(calculatorKey);
ClientRegistry.registerKeyBinding(jetpackKey);
ClientRegistry.registerKeyBinding(hudKey);
ClientRegistry.registerKeyBinding(reloadKey);
-
+
ClientRegistry.registerKeyBinding(craneUpKey);
ClientRegistry.registerKeyBinding(craneDownKey);
ClientRegistry.registerKeyBinding(craneLeftKey);
@@ -40,6 +44,9 @@ public class HbmKeybinds {
@SubscribeEvent
public void keyEvent(KeyInputEvent event) {
+ if (calculatorKey.getIsKeyPressed()) { // handle the calculator client-side only
+ FMLCommonHandler.instance().showGuiScreen(new GUICalculator());
+ }
HbmPlayerProps props = HbmPlayerProps.getData(MainRegistry.proxy.me());
diff --git a/src/main/java/com/hbm/inventory/gui/GUICalculator.java b/src/main/java/com/hbm/inventory/gui/GUICalculator.java
new file mode 100644
index 000000000..a8c5ec6e8
--- /dev/null
+++ b/src/main/java/com/hbm/inventory/gui/GUICalculator.java
@@ -0,0 +1,237 @@
+package com.hbm.inventory.gui;
+
+import com.hbm.lib.RefStrings;
+import net.minecraft.client.gui.GuiScreen;
+import net.minecraft.client.gui.GuiTextField;
+import net.minecraft.util.ResourceLocation;
+import org.lwjgl.input.Keyboard;
+import org.lwjgl.opengl.GL11;
+
+import java.math.BigDecimal;
+import java.util.Locale;
+import java.util.Stack;
+
+public class GUICalculator extends GuiScreen {
+ private static final ResourceLocation texture = new ResourceLocation(RefStrings.MODID, "textures/gui/calculator.png");
+ private int xSize = 220;
+ private int ySize = 50;
+ private GuiTextField inputField;
+
+ private String latestResult = "?";
+
+ @Override
+ public void initGui() {
+ Keyboard.enableRepeatEvents(true);
+
+ int x = (width - xSize) / 2;
+ int y = (height - ySize) / 2;
+ inputField = new GuiTextField(fontRendererObj, x + 5, y + 5, 210, 13);
+ inputField.setTextColor(-1);
+ inputField.setCanLoseFocus(false);
+ inputField.setFocused(true);
+ inputField.setMaxStringLength(1000);
+ }
+
+ @Override
+ public void onGuiClosed() {
+ Keyboard.enableRepeatEvents(false);
+ }
+
+ @Override
+ protected void keyTyped(char p_73869_1_, int p_73869_2_) {
+ if (!inputField.textboxKeyTyped(p_73869_1_, p_73869_2_))
+ super.keyTyped(p_73869_1_, p_73869_2_);
+
+ if (p_73869_1_ == 13 || p_73869_1_ == 10) { // when pressing enter (CR or LF)
+ try {
+ double result = Double.parseDouble(latestResult);
+ String plainStringRepresentation = (new BigDecimal(result)).toPlainString();
+ GuiScreen.setClipboardString(plainStringRepresentation);
+ inputField.setText(plainStringRepresentation);
+ inputField.setCursorPositionEnd();
+ inputField.setSelectionPos(0);
+ } catch (Exception ignored) {}
+ return;
+ }
+
+ String input = inputField.getText().replaceAll("[^\\d+\\-*/^.()\\sA-Za-z]+", "");
+
+ if (input.isEmpty()) {
+ latestResult = "?";
+ return;
+ }
+
+ try {
+ latestResult = Double.toString(evaluateExpression(input));
+ } catch (Exception e) { latestResult = e.toString(); }
+ }
+
+ @Override
+ public void drawScreen(int mouseX, int mouseY, float partialTicks) {
+ super.drawScreen(mouseX, mouseY, partialTicks);
+
+ GL11.glColor4f(1F, 1F, 1F, 1F);
+ mc.getTextureManager().bindTexture(texture);
+ int x = (width - xSize) / 2;
+ int y = (height - ySize) / 2;
+
+ drawTexturedModalRect(x, y, 0, 0, xSize, ySize);
+
+ inputField.drawTextBox();
+ fontRendererObj.drawString("=" + latestResult, x + 5, y + 30, -1);
+ }
+
+ /**
+ * Mathematically evaluates user-inputted strings
+ * It is recommended to catch all exceptions when using this
+ */
+ public static double evaluateExpression(String input) {
+ if (input.contains("^")) input = preEvaluatePower(input);
+
+ char[] tokens = input.toCharArray();
+ Stack values = new Stack<>();
+ Stack operators = new Stack<>();
+
+ for (int i = 0; i < tokens.length; i++) {
+ if (tokens[i] == ' ') continue;
+
+ if (tokens[i] >= '0' && tokens[i] <= '9' || tokens[i] == '.' || (tokens[i] == '-' && (i == 0 || "+-*/^(".contains(String.valueOf(tokens[i - 1]))))) {
+ StringBuilder buffer = new StringBuilder();
+ if (tokens[i] == '-') {
+ buffer.append('-'); // for negative numbers
+ i++;
+ }
+ while (i < tokens.length && (tokens[i] >= '0' && tokens[i] <= '9' || tokens[i] == '.')) buffer.append(tokens[i++]);
+ values.push(Double.parseDouble(buffer.toString()));
+ i--;
+ } else if (tokens[i] == '(') operators.push(Character.toString(tokens[i]));
+ else if (tokens[i] == ')') {
+ while (!operators.isEmpty() && operators.peek().charAt(0) != '(')
+ values.push(evaluateOperator(operators.pop().charAt(0), values.pop(), values.pop()));
+ operators.pop();
+ if (!operators.isEmpty() && operators.peek().length() > 1)
+ values.push(evaluateFunction(operators.pop(), values.pop()));
+ } else if (tokens[i] == '+' || tokens[i] == '-' || tokens[i] == '*' || tokens[i] == '/' || tokens[i] == '^') {
+ while (!operators.isEmpty() && hasPrecedence(String.valueOf(tokens[i]), operators.peek()))
+ values.push(evaluateOperator(operators.pop().charAt(0), values.pop(), values.pop()));
+ operators.push(Character.toString(tokens[i]));
+ } else if (tokens[i] >= 'A' && tokens[i] <= 'Z' || tokens[i] >= 'a' && tokens[i] <= 'z') {
+ StringBuilder charBuffer = new StringBuilder();
+ while (i < tokens.length && (tokens[i] >= 'A' && tokens[i] <= 'Z' || tokens[i] >= 'a' && tokens[i] <= 'z'))
+ charBuffer.append(tokens[i++]);
+ String string = charBuffer.toString();
+ if (string.equalsIgnoreCase("pi")) values.push(Math.PI);
+ else if (string.equalsIgnoreCase("e")) values.push(Math.E);
+ else operators.push(string.toLowerCase(Locale.ROOT));
+ i--;
+ }
+ }
+
+ // if the expression is correctly formatted, no function is remaining
+ while (!operators.empty()) values.push(evaluateOperator(operators.pop().charAt(0), values.pop(), values.pop()));
+
+ return values.pop();
+ }
+
+ private static double evaluateOperator(char operator, double x, double y) {
+ switch (operator) {
+ case '+': return y + x;
+ case '-': return y - x;
+ case '*': return y * x;
+ case '/': return y / x;
+
+ case '^': return Math.pow(y, x); // should not happen here, but oh well
+ }
+ return 0;
+ }
+
+ private static double evaluateFunction(String function, double x) {
+ switch (function) {
+ case "sqrt": return Math.sqrt(x);
+
+ case "sin": return Math.sin(x);
+ case "cos": return Math.cos(x);
+ case "tan": return Math.tan(x);
+ case "asin": return Math.asin(x);
+ case "acos": return Math.acos(x);
+ case "atan": return Math.atan(x);
+
+ case "log": return Math.log10(x);
+ case "ln": return Math.log(x);
+
+ case "ceil": return Math.ceil(x);
+ case "floor": return Math.floor(x);
+ case "round": return Math.round(x);
+ }
+ return 0;
+ }
+
+ /** Returns whether {@code second} has precedence over {@code first} */
+ private static boolean hasPrecedence(String first, String second) {
+ if (second.length() > 1) return false;
+
+ char firstChar = first.charAt(0);
+ char secondChar = second.charAt(0);
+
+ if (secondChar == '(' || secondChar == ')') return false;
+ else return (firstChar != '*' && firstChar != '/' && firstChar != '^') || (secondChar != '+' && secondChar != '-');
+ }
+
+ /** Returns the input with all powers evaluated */
+ private static String preEvaluatePower(String input) {
+ do {
+ int powerOperatorIndex = input.lastIndexOf('^');
+
+ // find base
+ boolean previousTokenIsParentheses = input.charAt(powerOperatorIndex - 1) == ')';
+ int parenthesesDepth = previousTokenIsParentheses ? 1 : 0;
+ int baseExpressionStart = previousTokenIsParentheses ? powerOperatorIndex - 2 : powerOperatorIndex - 1;
+ baseLoop:
+ for (; baseExpressionStart >= 0; baseExpressionStart--) { // search backwards
+ switch (input.charAt(baseExpressionStart)) {
+ case ')':
+ if (previousTokenIsParentheses) parenthesesDepth++;
+ else break baseLoop;
+ break;
+ case '(':
+ if (previousTokenIsParentheses && parenthesesDepth > 0) parenthesesDepth--;
+ else break baseLoop;
+ break;
+ case '+': case '-': case '*': case '/': case '^':
+ if (parenthesesDepth == 0) break baseLoop;
+ }
+ }
+ baseExpressionStart++; // go one token forward again
+ if (parenthesesDepth > 0) throw new IllegalArgumentException("Incomplete parentheses");
+
+ // find exponent
+ boolean nextTokenIsParentheses = input.charAt(powerOperatorIndex + 1) == '(';
+ parenthesesDepth = nextTokenIsParentheses ? 1 : 0;
+ int exponentExpressionEnd = nextTokenIsParentheses ? powerOperatorIndex + 2 : powerOperatorIndex + 1;
+ exponentLoop:
+ for (; exponentExpressionEnd < input.length(); exponentExpressionEnd++) {
+ switch (input.charAt(exponentExpressionEnd)) {
+ case '(':
+ if (nextTokenIsParentheses) parenthesesDepth++;
+ else break exponentLoop;
+ break;
+ case ')':
+ if (nextTokenIsParentheses && parenthesesDepth > 0) parenthesesDepth--;
+ else break exponentLoop;
+ break;
+ case '+': case '-': case '*': case '/': case '^':
+ if (parenthesesDepth == 0) break exponentLoop;
+ }
+ }
+ if (parenthesesDepth > 0) throw new IllegalArgumentException("Incomplete parentheses");
+
+ double base = evaluateExpression(input.substring(baseExpressionStart, powerOperatorIndex));
+ double exponent = evaluateExpression(input.substring(powerOperatorIndex + 1, exponentExpressionEnd));
+ double result = Math.pow(base, exponent);
+ // use big decimal to avoid scientific notation messing with the calculation
+ input = input.substring(0, baseExpressionStart) + (new BigDecimal(result)).toPlainString() + input.substring(exponentExpressionEnd);
+ } while (input.contains("^"));
+
+ return input;
+ }
+}
diff --git a/src/main/resources/assets/hbm/lang/de_DE.lang b/src/main/resources/assets/hbm/lang/de_DE.lang
index faa0d8d20..defbe5bb8 100644
--- a/src/main/resources/assets/hbm/lang/de_DE.lang
+++ b/src/main/resources/assets/hbm/lang/de_DE.lang
@@ -438,6 +438,7 @@ hazard.particleFine=Feinstaub
hazard.sand=Augenreizstoffe
hbm.key=NTM Hotkeys
+hbm.key.calculator=Taschenrechner
hbm.key.craneLoad=Kran laden/entladen
hbm.key.craneMoveDown=Kran rückwärts
hbm.key.craneMoveLeft=Kran nach links
diff --git a/src/main/resources/assets/hbm/lang/en_US.lang b/src/main/resources/assets/hbm/lang/en_US.lang
index 8bacaf62d..ab9f20e90 100644
--- a/src/main/resources/assets/hbm/lang/en_US.lang
+++ b/src/main/resources/assets/hbm/lang/en_US.lang
@@ -575,6 +575,7 @@ hazard.particleFine=Particulates
hazard.sand=Eye Irritants
hbm.key=NTM Hotkeys
+hbm.key.calculator=Calculator
hbm.key.craneLoad=Load/Unload Crane
hbm.key.craneMoveDown=Move Crane Backward
hbm.key.craneMoveLeft=Move Crane Left
diff --git a/src/main/resources/assets/hbm/textures/gui/calculator.png b/src/main/resources/assets/hbm/textures/gui/calculator.png
new file mode 100644
index 000000000..fa6fd968f
Binary files /dev/null and b/src/main/resources/assets/hbm/textures/gui/calculator.png differ