diff --git a/gradle.properties b/gradle.properties index 930c190..05ab990 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,6 +8,6 @@ org.gradle.jvmargs=-Xmx1G loader_version=0.14.23 # Mod Properties - mod_version = 3.0.20 + mod_version = 3.0.21 maven_group = ca.rttv archives_base_name = chatcalc diff --git a/src/main/java/ca/rttv/chatcalc/CallableFunction.java b/src/main/java/ca/rttv/chatcalc/CallableFunction.java deleted file mode 100644 index 18f96bd..0000000 --- a/src/main/java/ca/rttv/chatcalc/CallableFunction.java +++ /dev/null @@ -1,30 +0,0 @@ -package ca.rttv.chatcalc; - -import java.util.Optional; - -public record CallableFunction(String name, String rest, String[] params) { - public static Optional fromString(String str) { - int functionNameEnd = str.indexOf('('); - if (functionNameEnd <= 0) { - return Optional.empty(); - } - String functionName = str.substring(0, functionNameEnd); - int paramsEnd = str.substring(functionNameEnd).indexOf(')') + functionNameEnd; - if (!(functionName.matches("[A-Za-z]+") && paramsEnd > 0 && str.substring(paramsEnd + 1).startsWith("=") && str.length() > paramsEnd + 2)) { - return Optional.empty(); - } - String[] params = str.substring(functionNameEnd + 1, paramsEnd).split(ChatCalc.SEPARATOR); - for (String param : params) { - if (!param.matches("[A-Za-z]")) { - return Optional.empty(); - } - } - String rest = str.substring(paramsEnd + 2); - return Optional.of(new CallableFunction(functionName, rest, params)); - } - - @Override - public String toString() { - return name + '(' + String.join(ChatCalc.SEPARATOR, params) + ")=" + rest; - } -} diff --git a/src/main/java/ca/rttv/chatcalc/ChatCalc.java b/src/main/java/ca/rttv/chatcalc/ChatCalc.java index 179ddca..025cd73 100644 --- a/src/main/java/ca/rttv/chatcalc/ChatCalc.java +++ b/src/main/java/ca/rttv/chatcalc/ChatCalc.java @@ -1,55 +1,80 @@ package ca.rttv.chatcalc; +import com.mojang.datafixers.util.Either; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.widget.TextFieldWidget; +import net.minecraft.text.ClickEvent; +import net.minecraft.text.HoverEvent; +import net.minecraft.text.MutableText; import net.minecraft.text.Text; import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; import java.util.Optional; import java.util.regex.Pattern; -import java.util.stream.Collectors; public class ChatCalc { - public static final Pattern NUMBER = Pattern.compile("[-+]?\\d+(\\.\\d+)?"); + public static final Pattern NUMBER = Pattern.compile("[-+]?(\\d,?)+(\\.\\d+)?"); + public static final Pattern FUNCTION = Pattern.compile("[a-zA-Z]+\\(([a-zA-Z]+,)*?([a-zA-Z]+)\\)"); + public static final Pattern CONSTANT = Pattern.compile("[a-zA-Z]+"); public static final String SEPARATOR = ";"; public static final char SEPARATOR_CHAR = ';'; @Contract(value = "_->_", mutates = "param1") - public static boolean tryParse(TextFieldWidget field) { + public static boolean tryParse(@NotNull TextFieldWidget field) { final MinecraftClient client = MinecraftClient.getInstance(); String originalText = field.getText(); int cursor = field.getCursor(); - String text = ChatHelper.getWord(originalText, cursor); + String text = ChatHelper.getSection(originalText, cursor); { String[] split = text.split("="); if (split.length == 2) { if (Config.JSON.has(split[0])) { Config.JSON.addProperty(split[0], split[1]); Config.refreshJson(); - return ChatHelper.replaceWord(field, ""); + return ChatHelper.replaceSection(field, ""); } else { - Optional func = CallableFunction.fromString(text); - if (func.isPresent()) { - if (func.get().rest().isEmpty()) { - if (Config.FUNCTIONS.containsKey(func.get().name())) { - Config.FUNCTIONS.remove(func.get().name()); - } else { - return false; - } - } else { - Config.FUNCTIONS.put(func.get().name(), func.get()); + Optional> either = parseDeclaration(text); + if (either.isPresent()) { + Optional left = either.get().left(); + Optional right = either.get().right(); + if (left.isPresent()) { + Config.FUNCTIONS.put(left.get().name(), left.get()); + Config.refreshJson(); + return ChatHelper.replaceSection(field, ""); + } else if (right.isPresent()) { + Config.CONSTANTS.put(right.get().name(), right.get()); + Config.refreshJson(); + return ChatHelper.replaceSection(field, ""); } - Config.refreshJson(); - return ChatHelper.replaceWord(field, ""); } } } else if (split.length == 1) { if (Config.JSON.has(split[0])) { - return ChatHelper.replaceWord(field, Config.JSON.get(split[0]).getAsString()); + return ChatHelper.replaceSection(field, Config.JSON.get(split[0]).getAsString()); } else if (!split[0].isEmpty() && Config.JSON.has(split[0].substring(0, split[0].length() - 1)) && split[0].endsWith("?") && client.player != null) { client.player.sendMessage(Text.translatable("chatcalc." + split[0].substring(0, split[0].length() - 1) + ".description")); return false; + } else { + Optional> either = parseDeclaration(text); + if (either.isPresent()) { + Optional left = either.get().left(); + Optional right = either.get().right(); + if (left.isPresent()) { + if (Config.FUNCTIONS.containsKey(left.get().name())) { + Config.FUNCTIONS.remove(left.get().name()); + Config.refreshJson(); + return ChatHelper.replaceSection(field, ""); + } + } else if (right.isPresent()) { + if (Config.CONSTANTS.containsKey(right.get().name())) { + Config.CONSTANTS.remove(right.get().name()); + Config.refreshJson(); + return ChatHelper.replaceSection(field, ""); + } + } + } } } } @@ -61,7 +86,10 @@ public static boolean tryParse(TextFieldWidget field) { Testcases.test(Testcases.TESTCASES); return false; } else if (text.equals("functions?")) { - client.player.sendMessage(Text.literal("Currently defined custom functions are:\n" + Config.FUNCTIONS.values().stream().map(CallableFunction::toString).collect(Collectors.joining("\n")))); + client.player.sendMessage(Config.FUNCTIONS.values().stream().map(CustomFunction::toString).map(str -> Text.literal(str).styled(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, str)).withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.literal("Click to copy to clipboard"))))).collect(() -> Text.literal("Currently defined custom functions are:"), (a, b) -> a.append(Text.literal("\n").append(b)), MutableText::append)); + return false; + } else if (text.equals("constants?")) { + client.player.sendMessage(Config.CONSTANTS.values().stream().map(CustomConstant::toString).map(str -> Text.literal(str).styled(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, str)).withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.literal("Click to copy to clipboard"))))).collect(() -> Text.literal("Currently defined custom constants are:"), (a, b) -> a.append(Text.literal("\n").append(b)), MutableText::append)); return false; } else if (NUMBER.matcher(text).matches()) { return false; @@ -85,10 +113,14 @@ public static boolean tryParse(TextFieldWidget field) { } Config.saveToChatHud(originalText); Config.saveToClipboard(originalText); - return add ? ChatHelper.addWordAfterIndex(field, solution) : ChatHelper.replaceWord(field, solution); + return add ? ChatHelper.addSectionAfterIndex(field, solution) : ChatHelper.replaceSection(field, solution); } catch (Throwable t) { return false; } } } + + private static Optional> parseDeclaration(String text) { + return CustomFunction.fromString(text).map(Either::left).or(() -> CustomConstant.fromString(text).map(Either::right)); + } } diff --git a/src/main/java/ca/rttv/chatcalc/ChatHelper.java b/src/main/java/ca/rttv/chatcalc/ChatHelper.java index c33f7a1..e71686a 100644 --- a/src/main/java/ca/rttv/chatcalc/ChatHelper.java +++ b/src/main/java/ca/rttv/chatcalc/ChatHelper.java @@ -3,15 +3,15 @@ import net.minecraft.client.gui.widget.TextFieldWidget; public class ChatHelper { - public static String getWord(String input, int cursor) { - return input.substring(ChatHelper.getStartOfWord(input, cursor), ChatHelper.getEndOfWord(input, cursor)); + public static String getSection(String input, int cursor) { + return input.substring(ChatHelper.getStartOfSection(input, cursor), ChatHelper.getEndOfSection(input, cursor)); } - public static boolean replaceWord(TextFieldWidget field, String replacement) { + public static boolean replaceSection(TextFieldWidget field, String replacement) { String input = field.getText(); int cursor = field.getCursor(); - int start = ChatHelper.getStartOfWord(input, cursor); - int end = ChatHelper.getEndOfWord(input, cursor); + int start = ChatHelper.getStartOfSection(input, cursor); + int end = ChatHelper.getEndOfSection(input, cursor); String output = input.substring(0, start) + replacement + input.substring(end); if (output.length() > 256 || input.substring(start, end).equals(replacement)) { return false; @@ -20,9 +20,9 @@ public static boolean replaceWord(TextFieldWidget field, String replacement) { return true; } - public static boolean addWordAfterIndex(TextFieldWidget field, String word) { + public static boolean addSectionAfterIndex(TextFieldWidget field, String word) { String input = field.getText(); - int index = ChatHelper.getEndOfWord(input, field.getCursor()); + int index = ChatHelper.getEndOfSection(input, field.getCursor()); String output = input.substring(0, index) + word + input.substring(index); if (output.length() > 256) { return false; @@ -31,7 +31,7 @@ public static boolean addWordAfterIndex(TextFieldWidget field, String word) { return true; } - public static int getStartOfWord(String input, int cursor) { + public static int getStartOfSection(String input, int cursor) { if (cursor == 0) { return 0; } @@ -46,7 +46,7 @@ public static int getStartOfWord(String input, int cursor) { return 0; } - public static int getEndOfWord(String input, int cursor) { + public static int getEndOfSection(String input, int cursor) { if (cursor == input.length() - 1) { return cursor; } diff --git a/src/main/java/ca/rttv/chatcalc/Config.java b/src/main/java/ca/rttv/chatcalc/Config.java index 1989ea3..41118b3 100644 --- a/src/main/java/ca/rttv/chatcalc/Config.java +++ b/src/main/java/ca/rttv/chatcalc/Config.java @@ -14,7 +14,8 @@ public class Config { public static final JsonObject JSON; public static final Gson GSON; public static final File CONFIG_FILE; - public static final Map FUNCTIONS; + public static final Map FUNCTIONS; + public static final Map CONSTANTS; public static final ImmutableMap DEFAULTS; static { @@ -44,6 +45,7 @@ public class Config { } catch (IOException ignored) {} } FUNCTIONS = new HashMap<>(); + CONSTANTS = new HashMap<>(); if (CONFIG_FILE.exists() && CONFIG_FILE.isFile() && CONFIG_FILE.canRead()) { readJson(); } @@ -77,8 +79,10 @@ public static void refreshJson() { try { FileWriter writer = new FileWriter(CONFIG_FILE); JSON.add("functions", FUNCTIONS.values().stream().map(Object::toString).collect(JsonArray::new, JsonArray::add, JsonArray::addAll)); + JSON.add("constants", CONSTANTS.values().stream().map(Object::toString).collect(JsonArray::new, JsonArray::add, JsonArray::addAll)); writer.write(GSON.toJson(JSON)); JSON.remove("functions"); + JSON.remove("constants"); writer.close(); } catch (Exception ignored) { } } @@ -97,10 +101,17 @@ public static void readJson() { if (json.obj.get("functions") instanceof JsonArray array) { array.forEach(e -> { if (e instanceof JsonPrimitive primitive && primitive.isString()) { - CallableFunction.fromString(e.getAsString()).ifPresent(func -> FUNCTIONS.put(func.name(), func)); + CustomFunction.fromString(e.getAsString()).ifPresent(func -> FUNCTIONS.put(func.name(), func)); } }); } + if (json.obj.get("constants") instanceof JsonArray array) { + for (JsonElement e : array) { + if (e instanceof JsonPrimitive primitive && primitive.isString()) { + CustomConstant.fromString(e.getAsString()).ifPresent(constant -> CONSTANTS.put(constant.name(), constant)); + } + } + } } catch (Exception ignored) { } } @@ -114,12 +125,12 @@ public static void saveToChatHud(String input) { } public static double func(String name, double... values) { - CallableFunction func = FUNCTIONS.get(name); + CustomFunction func = FUNCTIONS.get(name); if (func != null) { if (values.length != func.params().length) { throw new IllegalArgumentException("Invalid amount of arguments for custom function"); } - String input = func.rest(); + String input = func.eval(); FunctionParameter[] parameters = new FunctionParameter[values.length]; for (int i = 0; i < parameters.length; i++) { parameters[i] = new FunctionParameter(func.params()[i], values[i]); diff --git a/src/main/java/ca/rttv/chatcalc/CustomConstant.java b/src/main/java/ca/rttv/chatcalc/CustomConstant.java new file mode 100644 index 0000000..01d0504 --- /dev/null +++ b/src/main/java/ca/rttv/chatcalc/CustomConstant.java @@ -0,0 +1,31 @@ +package ca.rttv.chatcalc; + +import java.util.Optional; + +public record CustomConstant(String name, String eval) { + + public static Optional fromString(String text) { + int equalsIdx = text.indexOf('='); + if (equalsIdx == -1) { + return Optional.empty(); + } + + String lhs = text.substring(0, equalsIdx); + String rhs = text.substring(equalsIdx + 1); + + if (!ChatCalc.CONSTANT.matcher(lhs).matches()) { + return Optional.empty(); + } + + return Optional.of(new CustomConstant(lhs, rhs)); + } + + public double value() { + return Config.makeEngine().eval(eval, new FunctionParameter[0]); + } + + @Override + public String toString() { + return name + "=" + eval; + } +} diff --git a/src/main/java/ca/rttv/chatcalc/CustomFunction.java b/src/main/java/ca/rttv/chatcalc/CustomFunction.java new file mode 100644 index 0000000..7c5254a --- /dev/null +++ b/src/main/java/ca/rttv/chatcalc/CustomFunction.java @@ -0,0 +1,27 @@ +package ca.rttv.chatcalc; + +import java.util.Optional; + +public record CustomFunction(String name, String eval, String[] params) { + public static Optional fromString(String text) { + int equalsIdx = text.indexOf('='); + if (equalsIdx == -1) { + return Optional.empty(); + } + String lhs = text.substring(0, equalsIdx); + String rhs = text.substring(equalsIdx + 1); + if (!ChatCalc.FUNCTION.matcher(lhs).matches()) { + return Optional.empty(); + } + int functionNameEnd = lhs.indexOf('('); + String functionName = lhs.substring(0, functionNameEnd); + int paramsEnd = lhs.substring(functionNameEnd).indexOf(')') + functionNameEnd; + String[] params = lhs.substring(functionNameEnd + 1, paramsEnd).split(ChatCalc.SEPARATOR); + return Optional.of(new CustomFunction(functionName, rhs, params)); + } + + @Override + public String toString() { + return name + '(' + String.join(ChatCalc.SEPARATOR, params) + ")=" + eval; + } +} diff --git a/src/main/java/ca/rttv/chatcalc/MathematicalConstant.java b/src/main/java/ca/rttv/chatcalc/MathematicalConstant.java new file mode 100644 index 0000000..2d6610e --- /dev/null +++ b/src/main/java/ca/rttv/chatcalc/MathematicalConstant.java @@ -0,0 +1,43 @@ +package ca.rttv.chatcalc; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.util.math.MathHelper; + +import java.util.LinkedHashSet; +import java.util.function.DoubleSupplier; + +public class MathematicalConstant { + public static final LinkedHashSet CONSTANTS = new LinkedHashSet<>(); + + static { + CONSTANTS.add(new MathematicalConstant("random", Math::random)); + CONSTANTS.add(new MathematicalConstant("rand", Math::random)); + CONSTANTS.add(new MathematicalConstant("rad", () -> Config.radians() ? 1.0 : 57.29577951308232)); + CONSTANTS.add(new MathematicalConstant("deg", () -> Config.radians() ? 0.017453292519943295 : 1.0)); + CONSTANTS.add(new MathematicalConstant("yaw", () -> Config.convertFromDegrees(MathHelper.wrapDegrees(MinecraftClient.getInstance().player.getYaw())))); + CONSTANTS.add(new MathematicalConstant("pitch", () -> Config.convertFromDegrees(MathHelper.wrapDegrees(MinecraftClient.getInstance().player.getPitch())))); + CONSTANTS.add(new MathematicalConstant("pi", () -> Math.PI)); + CONSTANTS.add(new MathematicalConstant("tau", () -> 2.0d * Math.PI)); + CONSTANTS.add(new MathematicalConstant("e", () -> Math.E)); + CONSTANTS.add(new MathematicalConstant("phi", () -> 1.6180339887498948482)); + CONSTANTS.add(new MathematicalConstant("x", () -> MinecraftClient.getInstance().player.getPos().x)); + CONSTANTS.add(new MathematicalConstant("y", () -> MinecraftClient.getInstance().player.getPos().y)); + CONSTANTS.add(new MathematicalConstant("z", () -> MinecraftClient.getInstance().player.getPos().z)); + } + + private final String name; + private final DoubleSupplier value; + + public MathematicalConstant(String name, DoubleSupplier value) { + this.name = name; + this.value = value; + } + + public String name() { + return name; + } + + public double value() { + return value.getAsDouble(); + } +} diff --git a/src/main/java/ca/rttv/chatcalc/MathematicalFunction.java b/src/main/java/ca/rttv/chatcalc/MathematicalFunction.java index 2d9bc01..f1015c0 100644 --- a/src/main/java/ca/rttv/chatcalc/MathematicalFunction.java +++ b/src/main/java/ca/rttv/chatcalc/MathematicalFunction.java @@ -1,56 +1,63 @@ package ca.rttv.chatcalc; +import com.google.common.math.DoubleMath; import net.minecraft.util.math.MathHelper; import java.util.HashMap; import java.util.Map; +import java.util.Optional; +import java.util.OptionalDouble; import java.util.function.DoubleUnaryOperator; +import java.util.function.Function; +import java.util.stream.DoubleStream; public final class MathematicalFunction { - public static final Map FUNCTIONS; + public static final Map> FUNCTIONS; static { FUNCTIONS = new HashMap<>(); - FUNCTIONS.put("sqrt", Math::sqrt); - FUNCTIONS.put("cbrt", Math::cbrt); - - FUNCTIONS.put("sin", val -> Math.sin(Config.convertToRadians(val))); - FUNCTIONS.put("cos", val -> Math.cos(Config.convertToRadians(val))); - FUNCTIONS.put("tan", val -> Math.tan(Config.convertToRadians(val))); - FUNCTIONS.put("csc", val -> 1 / Math.sin(Config.convertToRadians(val))); - FUNCTIONS.put("sec", val -> 1 / Math.cos(Config.convertToRadians(val))); - FUNCTIONS.put("cot", val -> 1 / Math.tan(Config.convertToRadians(val))); - - FUNCTIONS.put("arcsin", val -> Config.convertFromRadians(Math.asin(val))); - FUNCTIONS.put("asin", val -> Config.convertFromRadians(Math.asin(val))); - - FUNCTIONS.put("acos", val -> Config.convertFromRadians(Math.acos(val))); - FUNCTIONS.put("arccos", val -> Config.convertFromRadians(Math.acos(val))); - - FUNCTIONS.put("atan", val -> Config.convertFromRadians(Math.atan(val))); - FUNCTIONS.put("arctan", val -> Config.convertFromRadians(Math.atan(val))); - - FUNCTIONS.put("arccsc", val -> Config.convertFromRadians(Math.asin(1 / val))); - FUNCTIONS.put("acsc", val -> Config.convertFromRadians(Math.asin(1/ val))); - - FUNCTIONS.put("arcsec", val -> Config.convertFromRadians(Math.acos(1 / val))); - FUNCTIONS.put("asec", val -> Config.convertFromRadians(Math.acos(1/ val))); - - FUNCTIONS.put("arccot", val -> Config.convertFromRadians(Math.atan(1 / val))); - FUNCTIONS.put("acot", val -> Config.convertFromRadians(Math.atan(1/ val))); - - FUNCTIONS.put("floor", Math::floor); - FUNCTIONS.put("ceil", Math::ceil); - FUNCTIONS.put("round", x -> Math.floor(x + 0.5d)); - FUNCTIONS.put("abs", Math::abs); - FUNCTIONS.put("log", Math::log10); - FUNCTIONS.put("ln", Math::log); - FUNCTIONS.put("exp", Math::exp); - - FUNCTIONS.put("min", DoubleUnaryOperator.identity()); - FUNCTIONS.put("max", DoubleUnaryOperator.identity()); - FUNCTIONS.put("clamp", DoubleUnaryOperator.identity()); - FUNCTIONS.put("cmp", DoubleUnaryOperator.identity()); + + FUNCTIONS.put("sqrt", simple(Math::sqrt)); + FUNCTIONS.put("cbrt", simple(Math::cbrt)); + + FUNCTIONS.put("sin", simple(val -> Math.sin(Config.convertToRadians(val)))); + FUNCTIONS.put("cos", simple(val -> Math.cos(Config.convertToRadians(val)))); + FUNCTIONS.put("tan", simple(val -> Math.tan(Config.convertToRadians(val)))); + FUNCTIONS.put("csc", simple(val -> 1 / Math.sin(Config.convertToRadians(val)))); + FUNCTIONS.put("sec", simple(val -> 1 / Math.cos(Config.convertToRadians(val)))); + FUNCTIONS.put("cot", simple(val -> 1 / Math.tan(Config.convertToRadians(val)))); + + FUNCTIONS.put("arcsin", simple(val -> Config.convertFromRadians(Math.asin(val)))); + FUNCTIONS.put("asin", FUNCTIONS.get("arcsin")); + + FUNCTIONS.put("arccos", simple(val -> Config.convertFromRadians(Math.acos(val)))); + FUNCTIONS.put("acos", FUNCTIONS.get("arccos")); + + FUNCTIONS.put("arctan", simple(val -> Config.convertFromRadians(Math.atan(val)))); + FUNCTIONS.put("atan", FUNCTIONS.get("arctan")); + + FUNCTIONS.put("arccsc", simple(val -> Config.convertFromRadians(Math.asin(1 / val)))); + FUNCTIONS.put("acsc", FUNCTIONS.get("arccsc")); + + FUNCTIONS.put("arcsec", simple(val -> Config.convertFromRadians(Math.acos(1 / val)))); + FUNCTIONS.put("asec", FUNCTIONS.get("arcsec")); + + FUNCTIONS.put("arccot", simple(val -> Config.convertFromRadians(Math.atan(1 / val)))); + FUNCTIONS.put("acot", FUNCTIONS.get("arccot")); + + FUNCTIONS.put("floor", simple(Math::floor)); + FUNCTIONS.put("ceil", simple(Math::ceil)); + FUNCTIONS.put("round", simple(x -> Math.floor(x + 0.5d))); + FUNCTIONS.put("abs", simple(Math::abs)); + FUNCTIONS.put("log", simple(Math::log10)); + FUNCTIONS.put("ln", simple(Math::log)); + FUNCTIONS.put("exp", simple(Math::exp)); + + FUNCTIONS.put("sgn", simple(x -> Double.isNaN(x) || x + 0.0 == 0.0 ? 0.0 : (x >= 0.0 ? 1.0 : -1.0))); + FUNCTIONS.put("min", values -> DoubleStream.of(values).min()); + FUNCTIONS.put("max", values -> DoubleStream.of(values).max()); + FUNCTIONS.put("clamp", values -> values.length == 3 ? OptionalDouble.of(MathHelper.clamp(values[0], values[1], values[2])) : OptionalDouble.empty()); + FUNCTIONS.put("cmp", values -> (values.length >= 2 && values.length <= 3) ? OptionalDouble.of((Math.abs(values[0] - values[1]) <= (values.length == 2 ? 0.0 : values[2])) ? 0.0d : (values[0] < values[1] ? -1.0d : (values[0] > values[1] ? 1.0d : 0.0d))) : OptionalDouble.empty()); } public final String func; @@ -65,65 +72,29 @@ public MathematicalFunction(String value) { } public double apply(double... values) { - if (func.equals("cmp")) { - if (values.length < 2 || values.length > 3) { - throw new IllegalArgumentException(); - } - double epsilon = values.length == 2 ? 0.0 : values[2]; - double a = values[0]; - double b = values[1]; - if (Math.abs(a - b) <= epsilon) { - return 0.0; - } else if (a < b) { - return -1.0; - } else if (a > b) { - return 1.0; - } else { - return 0.0; - } - } - - if (func.equals("min")) { - if (values.length == 0) { - throw new IllegalArgumentException(); - } - double min = values[0]; - for (double value : values) { - min = Math.min(min, value); - } - return min; - } - - if (func.equals("max")) { - if (values.length == 0) { - throw new IllegalArgumentException(); - } - double max = values[0]; - for (double value : values) { - max = Math.max(max, value); - } - return max; - } - - if (func.equals("clamp")) { - if (values.length != 3) { - throw new IllegalArgumentException(); - } - return MathHelper.clamp(values[0], values[1], values[2]); - } - - if (FUNCTIONS.containsKey(func)) { - if (values.length != 1) { - throw new IllegalArgumentException(); - } - - return FUNCTIONS.get(func).applyAsDouble(values[0]); - } else { - return Config.func(func, values); - } + return Optional.ofNullable(FUNCTIONS.get(func)).map(function -> function.apply(values).orElseThrow(IllegalArgumentException::new)).orElseGet(() -> Config.func(func, values)); } public static double log(double base, double value) { return Math.log(value) / Math.log(base); } + + public static double factorial(double x) { + return x % 1.0d == 0.0d & x >= 1.0d + ? DoubleMath.factorial((int) x) + : Math.sqrt(2.0 * Math.PI * x) + * Math.pow(x / Math.E, x) + * (1.0d + + 1.0d / (12.0d * x) + + 1.0d / (288.0d * x * x) + - 139.0d / (51840.0d * x * x * x) + - 571.0d / (2488320.0d * x * x * x * x) + + 163879.0d / (209018880.0d * x * x * x * x * x) + + 5246819.0d / (75246796800.0d * x * x * x * x * x * x) + + -534703531.0d / (902961561600.0d * x * x * x * x * x * x * x)); + } + + private static Function simple(DoubleUnaryOperator simple) { + return values -> values.length == 1 ? OptionalDouble.of(simple.applyAsDouble(values[0])) : OptionalDouble.empty(); + } } \ No newline at end of file diff --git a/src/main/java/ca/rttv/chatcalc/NibbleMathEngine.java b/src/main/java/ca/rttv/chatcalc/NibbleMathEngine.java index aeef936..14545e8 100644 --- a/src/main/java/ca/rttv/chatcalc/NibbleMathEngine.java +++ b/src/main/java/ca/rttv/chatcalc/NibbleMathEngine.java @@ -1,32 +1,24 @@ package ca.rttv.chatcalc; import com.google.common.collect.Streams; -import net.minecraft.client.MinecraftClient; -import net.minecraft.util.math.MathHelper; -import net.minecraft.util.math.Vec3d; import java.nio.charset.StandardCharsets; +import static ca.rttv.chatcalc.MathematicalFunction.factorial; + public class NibbleMathEngine implements MathEngine { byte[] bytes; int idx; FunctionParameter[] params; - double x, y, z, yaw, pitch; + boolean abs; @Override public double eval(String input, FunctionParameter[] paramaters) { - final MinecraftClient client = MinecraftClient.getInstance(); bytes = fixParenthesis(input).concat("\0").getBytes(StandardCharsets.US_ASCII); // we shouldn't encounter unicode in our math idx = 0; + abs = false; params = paramaters; - //noinspection DataFlowIssue -- player != null when the chat != null - Vec3d pos = client.player.getPos(); - x = pos.x; - y = pos.y; - z = pos.z; - yaw = Config.convertFromDegrees(MathHelper.wrapDegrees(client.player.getYaw())); - pitch = Config.convertFromDegrees(MathHelper.wrapDegrees(client.player.getPitch())); - double result = expression(false); + double result = expression(); if (idx + 1 != bytes.length) { throw new IllegalArgumentException("Evaluation had unexpected remaining characters"); } @@ -61,47 +53,33 @@ private boolean bite(char bite) { // pun intended } } - private double expression(boolean abs) { - double x = modulo(abs); + private double expression() { + double x = modulo(); while (true) { - if (bite('+')) x += modulo(abs); - else if (bite('-')) x -= modulo(abs); + if (bite('+')) x += modulo(); + else if (bite('-')) x -= modulo(); else return x; } } - private double modulo(boolean abs) { - double x = term(abs); - while (true) { - if (bite('%')) { - double b = term(abs); - double r = x % b; - if (r < 0.0) r += Math.abs(b); - x = r; - } else return x; - } - } - - private double term(boolean abs) { - double x = grouping(abs); + private double modulo() { + double x = term(); while (true) { - if (bite('*')) x *= grouping(abs); - else if (bite('/')) x /= grouping(abs); - else if (bytes[idx] <= '9' & bytes[idx] >= '0') x *= expression(abs); - else if (!abs & bytes[idx] == '|') x *= Math.abs(expression(false)); // simplify to false + if (bite('%')) x = Math.IEEEremainder(x, term()); else return x; } } - private double grouping(boolean abs) { - double x = part(abs); + private double term() { + double x = grouping(); while (true) { - if (bytes[idx] == '(') x *= expression(abs); + if (bite('*')) x *= grouping(); + else if (bite('/')) x /= grouping(); else return x; } } - private double part(boolean abs) { + private double grouping() { long sign = 0L; while (bytes[idx] == '+' | bytes[idx] == '-') { if (bytes[idx++] == '-') { @@ -109,141 +87,99 @@ private double part(boolean abs) { } } - double x = 1.0; + double x = part(); + while (isStartOfPart(bytes[idx])) { + x *= part(); + } - a: - { - boolean somethingParsed = false; - if (bite('(')) { - x = expression(false); - somethingParsed = true; - if (!bite(')')) throw new IllegalArgumentException("Expected closing parenthesis"); - } else if (!abs && bite('|')) { - x = Math.abs(expression(true)); - somethingParsed = true; - if (!bite('|')) throw new IllegalArgumentException("Expected closing absolute value character"); - } else if ((bytes[idx] <= '9' & bytes[idx] >= '0') | bytes[idx] == '.' | bytes[idx] == ',') { - int start = idx; - while ((bytes[idx] <= '9' & bytes[idx] >= '0') | bytes[idx] == '.' | bytes[idx] == ',') idx++; - x = Double.parseDouble(new String(bytes, start, idx - start, StandardCharsets.US_ASCII).replace(",", "")); - somethingParsed = true; - } - if (bytes[idx] <= 'z' & bytes[idx] >= 'a') { - int start = idx; - while (bytes[idx] <= 'z' & bytes[idx] >= 'a' | bytes[idx] == '_') idx++; - String func = new String(bytes, start, idx - start, StandardCharsets.US_ASCII); - double u = 1.0; - b: - while (true) { - if (func.startsWith("random")) { - x *= u; - u = Math.random(); - func = func.substring(6); - if (!bite('(')) throw new IllegalArgumentException(); - bite(')'); - continue; - } - if (func.startsWith("rand")) { - x *= u; - u = Math.random(); - func = func.substring(4); - if (!bite('(')) throw new IllegalArgumentException(); - bite(')'); - continue; - } - if (func.startsWith("rad")) { - x *= u; - u = Config.radians() ? 1.0 : 57.29577951308232; - func = func.substring(3); - continue; - } - if (func.startsWith("deg")) { - x *= u; - u = Config.radians() ? 0.017453292519943295 : 1.0; - func = func.substring(3); - continue; - } - if (func.startsWith("yaw")) { - x *= u; - u = this.yaw; - func = func.substring(3); - continue; - } - if (func.startsWith("pitch")) { - x *= u; - u = this.pitch; - func = func.substring(5); - continue; - } - if (func.startsWith("pi")) { - x *= u; - u = Math.PI; - func = func.substring(2); - continue; - } - if (func.startsWith("tau")) { - x *= u; - u = Math.PI * 2; - func = func.substring(2); - continue; - } - if (func.startsWith("e")) { - x *= u; - u = Math.E; - func = func.substring(1); - continue; - } - if (func.startsWith("phi")) { - x *= u; - u = 1.6180339887498948482; - func = func.substring(2); - continue; - } - if (func.startsWith("x")) { - x *= u; - u = this.x; - func = func.substring(1); - continue; - } - if (func.startsWith("y")) { - x *= u; - u = this.y; - func = func.substring(1); - continue; - } - if (func.startsWith("z")) { - x *= u; - u = this.z; - func = func.substring(1); - continue; - } - for (FunctionParameter param : params) { - if (func.startsWith(param.name()) && Streams.concat(Config.FUNCTIONS.keySet().stream(), MathematicalFunction.FUNCTIONS.keySet().stream()).noneMatch(func::startsWith)) { - x *= u; - u = param.value(); - func = func.substring(param.name().length()); - continue b; - } + return Double.longBitsToDouble(Double.doubleToLongBits(x) ^ sign); + } + + private double part() { + double x = meat(); + + if (bite('!')) x = factorial(x); + if (bite('^')) { + boolean absBefore = abs; + abs = false; + x = Math.pow(x, grouping()); + abs = absBefore; + } + + return x; + } + + // should not be called at all, except once in `part`, meant for easy return statement usage, not to be called + private double meat() { + if (bite('(')) { + boolean absBefore = abs; + abs = false; + double x = expression(); + if (!bite(')')) throw new IllegalArgumentException("Expected closing parenthesis"); + abs = absBefore; + return x; + } + + if (!abs && bite('|')) { + boolean absBefore = abs; + abs = true; + double x = Math.abs(expression()); + if (!bite('|')) throw new IllegalArgumentException("Expected closing absolute value character"); + abs = absBefore; + return x; + } + + if ((bytes[idx] <= '9' & bytes[idx] >= '0') | bytes[idx] == '.' | bytes[idx] == ',') { + int start = idx; + while ((bytes[idx] <= '9' & bytes[idx] >= '0') | bytes[idx] == '.' | bytes[idx] == ',') idx++; + return Double.parseDouble(new String(bytes, start, idx - start, StandardCharsets.US_ASCII).replace(",", "")); + } + + if (bytes[idx] >= 'a' & bytes[idx] <= 'z') { + int start = idx; + while (bytes[idx] <= 'z' & bytes[idx] >= 'a') idx++; + if (bytes[idx] == '_') idx++; + String func = new String(bytes, start, idx - start, StandardCharsets.US_ASCII); + + if (Streams.concat(Config.FUNCTIONS.keySet().stream(), MathematicalFunction.FUNCTIONS.keySet().stream()).noneMatch(func::startsWith)) { + for (FunctionParameter param : params) { + if (func.startsWith(param.name())) { + idx -= func.length() - param.name().length(); + return param.value(); } - if (func.isEmpty()) { - if (bite('^')) u = Math.pow(u, part(false)); - x *= u; - break a; + } + + for (MathematicalConstant constant : MathematicalConstant.CONSTANTS) { + if (func.startsWith(constant.name())) { + idx -= func.length() - constant.name().length(); + return constant.value(); } - break; } - if (func.equals("log_")) { - double base = part(false); - if (!bite('(')) throw new IllegalArgumentException("Expected parenthesis for logarithmic function"); - double value = expression(false); - if (!bite(')')) - throw new IllegalArgumentException("Expected closing parenthesis for logarithmic function"); - x *= MathematicalFunction.log(base, value); - break a; + + for (CustomConstant constant : Config.CONSTANTS.values()) { + if (func.startsWith(constant.name())) { + idx -= func.length() - constant.name().length(); + return constant.value(); + } } + } + + if (func.equals("log_")) { + boolean absBefore = abs; + abs = false; + double base = part(); + if (!bite('(')) throw new IllegalArgumentException("Expected parenthesis for logarithmic function"); + double value = expression(); + if (!bite(')')) throw new IllegalArgumentException("Expected closing parenthesis for logarithmic function"); + abs = absBefore; + return MathematicalFunction.log(base, value); + } + + { + boolean absBefore = abs; + abs = false; int param_count = 1; - double exponent = 1.0; - if (bite('^')) exponent = part(false); + double exponent = bite('^') ? grouping() : 1.0d; if (!bite('(')) throw new IllegalArgumentException("Expected parenthesis for function"); int depth = 0; int before = idx; @@ -259,21 +195,25 @@ else if (bytes[idx] == ')') { int value_count = 0; while (true) { if (bite('\0')) throw new IllegalArgumentException("Expected closing parenthesis for function"); - values[value_count++] = expression(false); + values[value_count++] = expression(); if (bite(')')) break; - if (!bite(';')) throw new IllegalArgumentException("Expected that a semicolon exists between the parameters"); + if (!bite(';')) + throw new IllegalArgumentException("Expected that a semicolon exists between the parameters"); } - x *= Math.pow(new MathematicalFunction(func).apply(values), exponent); - somethingParsed = true; - } - - if (!somethingParsed) { - throw new IllegalArgumentException("Expected a valid character for equation, not " + (char) bytes[idx]); + abs = absBefore; + return Math.pow(new MathematicalFunction(func).apply(values), exponent); } } - if (bite('^')) x = Math.pow(x, part(false)); + throw new IllegalArgumentException("Expected a valid character for equation, not '" + (char) bytes[idx] + "' (at index " + idx + ")"); + } + + private boolean isStartOfPart(byte c) { + return (c >= 'a' & c <= 'z') | (c >= '0' & c <= '9') | (c == '(') | (c == '|' & !abs); + } - return Double.longBitsToDouble(Double.doubleToLongBits(x) | sign); + @Override + public String toString() { + return new String(bytes, idx, bytes.length - idx - 1); } } diff --git a/src/main/java/ca/rttv/chatcalc/Testcases.java b/src/main/java/ca/rttv/chatcalc/Testcases.java index e18f34c..9429700 100644 --- a/src/main/java/ca/rttv/chatcalc/Testcases.java +++ b/src/main/java/ca/rttv/chatcalc/Testcases.java @@ -54,7 +54,7 @@ public interface Testcases { new Pair<>("|-2.5-0.1|", 2.6d), new Pair<>("0.5|-2.5-0.1|", 1.3d), new Pair<>("5%360", 5.0d), - new Pair<>("-5%360", 355.0d), + new Pair<>("-5%360", -5.0d), // add the two remaining signed ones new Pair<>("min(sqrt(37);6", 6.0d), new Pair<>("max(sqrt(37);7", 7.0d), @@ -79,7 +79,7 @@ static void test(List> list) { client.player.sendMessage(Text.literal("§cTest case §n§cfailed: " + entry.getLeft() + ", expected " + entry.getRight() + ", got " + result)); } } catch (Exception e) { - client.player.sendMessage(Text.literal("§aTest case failed with exception: " + entry.getLeft() + ", expected " + entry.getRight() + ", got " + e)); + client.player.sendMessage(Text.literal("§cTest case failed with exception: " + entry.getLeft() + ", expected " + entry.getRight() + ", got " + e)); } } } diff --git a/src/main/java/ca/rttv/chatcalc/mixin/ChatScreenMixin.java b/src/main/java/ca/rttv/chatcalc/mixin/ChatScreenMixin.java index fd0e38c..daaa01c 100644 --- a/src/main/java/ca/rttv/chatcalc/mixin/ChatScreenMixin.java +++ b/src/main/java/ca/rttv/chatcalc/mixin/ChatScreenMixin.java @@ -3,7 +3,6 @@ import ca.rttv.chatcalc.ChatCalc; import ca.rttv.chatcalc.Config; import ca.rttv.chatcalc.duck.ChatInputSuggesterDuck; -import com.mojang.brigadier.suggestion.Suggestions; import net.minecraft.client.gui.screen.ChatInputSuggestor; import net.minecraft.client.gui.screen.ChatScreen; import net.minecraft.client.gui.widget.TextFieldWidget; @@ -13,8 +12,6 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -import java.util.concurrent.CompletableFuture; - @Mixin(ChatScreen.class) abstract class ChatScreenMixin { @Shadow @@ -25,9 +22,7 @@ abstract class ChatScreenMixin { @Inject(at = @At("HEAD"), method = "keyPressed(III)Z", cancellable = true) private void keyPressed(int keyCode, int scanCode, int modifiers, CallbackInfoReturnable cir) { - CompletableFuture suggestions = ((ChatInputSuggesterDuck) this.chatInputSuggestor).chatcalc$pendingSuggestions(); - // I have never dealt with CompletableFuture before, so I don't know if there's a method to check if everything went well - if (!Config.calculateLast() || (suggestions != null && suggestions.isDone() && !suggestions.isCompletedExceptionally() && suggestions.getNow(null).isEmpty())) { + if (!Config.calculateLast() || ((ChatInputSuggesterDuck) this.chatInputSuggestor).chatcalc$pendingSuggestions().join().isEmpty()) { if (keyCode == 258 && ChatCalc.tryParse(chatField)) { cir.setReturnValue(true); } diff --git a/src/main/java/ca/rttv/chatcalc/mixin/TextFieldWidgetMixin.java b/src/main/java/ca/rttv/chatcalc/mixin/TextFieldWidgetMixin.java index 0612e7d..a99d0f6 100644 --- a/src/main/java/ca/rttv/chatcalc/mixin/TextFieldWidgetMixin.java +++ b/src/main/java/ca/rttv/chatcalc/mixin/TextFieldWidgetMixin.java @@ -6,8 +6,10 @@ import ca.rttv.chatcalc.FunctionParameter; import net.minecraft.client.font.TextRenderer; import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.widget.ClickableWidget; import net.minecraft.client.gui.widget.TextFieldWidget; import net.minecraft.text.Text; +import net.minecraft.text.TranslatableTextContent; import net.minecraft.util.Pair; import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Final; @@ -20,7 +22,11 @@ import org.spongepowered.asm.mixin.injection.callback.LocalCapture; @Mixin(TextFieldWidget.class) -abstract class TextFieldWidgetMixin { +abstract class TextFieldWidgetMixin extends ClickableWidget { + public TextFieldWidgetMixin(int x, int y, int width, int height, Text message) { + super(x, y, width, height, message); + } + @Shadow @Final private TextRenderer textRenderer; @Shadow public native int getCursor(); @@ -43,12 +49,16 @@ private void renderButton1202(DrawContext context, int mouseX, int mouseY, float @Unique private void chatcalc$displayAbove(DrawContext context, int x, int y) { + if (!(getMessage().getContent() instanceof TranslatableTextContent translatable && translatable.getKey().equals("chat.editBox"))) { + return; + } + if (!Config.displayAbove()) { evaluationCache = null; return; } - String word = ChatHelper.getWord(getText(), getCursor()); + String word = ChatHelper.getSection(getText(), getCursor()); if (ChatCalc.NUMBER.matcher(word).matches()) { evaluationCache = null;