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