diff --git a/src/main/java/net/neganote/gtutilities/common/gui/widgets/MultilineTextField.java b/src/main/java/net/neganote/gtutilities/common/gui/widgets/MultilineTextField.java index 9701092..b28b80d 100644 --- a/src/main/java/net/neganote/gtutilities/common/gui/widgets/MultilineTextField.java +++ b/src/main/java/net/neganote/gtutilities/common/gui/widgets/MultilineTextField.java @@ -32,6 +32,7 @@ public class MultilineTextField extends WidgetGroup { private final Supplier textSupplier; private final Consumer textConsumer; private final Component placeholder; + private final Supplier borderColorSupplier; private int maxLength = DEFAULT_MAX_LENGTH; @@ -50,7 +51,7 @@ public MultilineTextField( int x, int y, int width, int height, Supplier textSupplier, Consumer textConsumer) { - this(x, y, width, height, textSupplier, textConsumer, Component.empty()); + this(x, y, width, height, textSupplier, textConsumer, Component.empty(), null); } public MultilineTextField( @@ -58,10 +59,20 @@ public MultilineTextField( Supplier textSupplier, Consumer textConsumer, Component placeholder) { + this(x, y, width, height, textSupplier, textConsumer, placeholder, null); + } + + public MultilineTextField( + int x, int y, int width, int height, + Supplier textSupplier, + Consumer textConsumer, + Component placeholder, + Supplier borderColorSupplier) { super(new Position(x, y), new Size(width, height)); this.textSupplier = textSupplier; this.textConsumer = textConsumer; this.placeholder = placeholder == null ? Component.empty() : placeholder; + this.borderColorSupplier = borderColorSupplier; String init = safe(textSupplier.get()); this.lastSent = init; @@ -467,7 +478,8 @@ public void drawInBackground(net.minecraft.client.gui.GuiGraphics graphics, int int h = getSize().height; int bg = 0xFF202020; - int border = hasFocus ? 0xFFFFFFFF : 0xFF808080; + Integer suppliedBorder = borderColorSupplier != null ? borderColorSupplier.get() : null; + int border = suppliedBorder != null ? suppliedBorder : hasFocus ? 0xFFFFFFFF : 0xFF808080; graphics.fill(x0 - 1, y0 - 1, x0 + w + 1, y0 + h + 1, 0xAA000000); graphics.fill(x0, y0, x0 + w, y0 + h, bg); diff --git a/src/main/java/net/neganote/gtutilities/integration/ae2/machine/MEEnlargedTagStockingInputBusPartMachine.java b/src/main/java/net/neganote/gtutilities/integration/ae2/machine/MEEnlargedTagStockingInputBusPartMachine.java index 59485a0..139d9fd 100644 --- a/src/main/java/net/neganote/gtutilities/integration/ae2/machine/MEEnlargedTagStockingInputBusPartMachine.java +++ b/src/main/java/net/neganote/gtutilities/integration/ae2/machine/MEEnlargedTagStockingInputBusPartMachine.java @@ -53,7 +53,11 @@ import it.unimi.dsi.fastutil.objects.Object2LongMap; import org.jetbrains.annotations.Nullable; -import java.util.*; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.PriorityQueue; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Predicate; @@ -75,6 +79,12 @@ public class MEEnlargedTagStockingInputBusPartMachine extends MEStockingBusPartM @DescSynced protected String blacklistExpr = ""; + @DescSynced + protected boolean whitelistBadSyntax = false; + + @DescSynced + protected boolean blacklistBadSyntax = false; + @DescSynced protected String Wltmp = ""; @@ -168,11 +178,13 @@ private void ensureCompiledUpToDate() { if (!Objects.equals(wl, wlLast)) { wlLast = wl; wlCompiled = TagMatcher.compile(wl); + whitelistBadSyntax = !wlCompiled.isValid(); decisionCache.clear(); } if (!Objects.equals(bl, blLast)) { blLast = bl; blCompiled = TagMatcher.compile(bl); + blacklistBadSyntax = !blCompiled.isValid(); decisionCache.clear(); } @@ -183,6 +195,7 @@ private void ensureCompiledUpToDate() { protected boolean isAllowed(AEItemKey key) { ensureCompiledUpToDate(); + if (whitelistBadSyntax || blacklistBadSyntax) return false; if ((wlLast == null || wlLast.isEmpty()) && (blLast == null || blLast.isEmpty())) return false; @@ -380,7 +393,8 @@ public boolean mouseWheelMove(double mouseX, double mouseY, double wheelDelta) { 7, y, 160, 25, () -> Wltmp, v -> { Wltmp = v; }, - Component.literal("Whitelist tags...")); + Component.literal("Whitelist tags..."), + () -> whitelistBadSyntax ? 0xFFFF0000 : null); group.addWidget(WLField); y += 29; @@ -388,7 +402,8 @@ public boolean mouseWheelMove(double mouseX, double mouseY, double wheelDelta) { 7, y, 160, 25, () -> Bltmp, v -> { Bltmp = v; }, - Component.literal("Blacklist tags...")); + Component.literal("Blacklist tags..."), + () -> blacklistBadSyntax ? 0xFFFF0000 : null); group.addWidget(BLField); WLField.setDirectly(whitelistExpr); diff --git a/src/main/java/net/neganote/gtutilities/integration/ae2/machine/MEEnlargedTagStockingInputHatchPartMachine.java b/src/main/java/net/neganote/gtutilities/integration/ae2/machine/MEEnlargedTagStockingInputHatchPartMachine.java index 4c83e35..c21f35e 100644 --- a/src/main/java/net/neganote/gtutilities/integration/ae2/machine/MEEnlargedTagStockingInputHatchPartMachine.java +++ b/src/main/java/net/neganote/gtutilities/integration/ae2/machine/MEEnlargedTagStockingInputHatchPartMachine.java @@ -12,7 +12,9 @@ import com.gregtechceu.gtceu.config.ConfigHolder; import com.gregtechceu.gtceu.integration.ae2.gui.widget.AEFluidConfigWidget; import com.gregtechceu.gtceu.integration.ae2.machine.MEStockingHatchPartMachine; -import com.gregtechceu.gtceu.integration.ae2.slot.*; +import com.gregtechceu.gtceu.integration.ae2.slot.ExportOnlyAEFluidList; +import com.gregtechceu.gtceu.integration.ae2.slot.ExportOnlyAEFluidSlot; +import com.gregtechceu.gtceu.integration.ae2.slot.ExportOnlyAESlot; import com.gregtechceu.gtceu.integration.ae2.utils.AEUtil; import com.lowdragmc.lowdraglib.gui.texture.GuiTextureGroup; @@ -50,7 +52,11 @@ import it.unimi.dsi.fastutil.objects.Object2LongMap; import org.jetbrains.annotations.Nullable; -import java.util.*; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.PriorityQueue; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Predicate; @@ -75,6 +81,12 @@ public class MEEnlargedTagStockingInputHatchPartMachine extends MEStockingHatchP @DescSynced protected String Bltmp = ""; + @DescSynced + protected boolean whitelistBadSyntax = false; + + @DescSynced + protected boolean blacklistBadSyntax = false; + private Predicate tagAutoPullTest = ($) -> true; private transient String wlLast = null; @@ -161,11 +173,13 @@ private void ensureCompiledUpToDate() { if (!Objects.equals(wl, wlLast)) { wlLast = wl; wlCompiled = TagMatcher.compile(wl); + whitelistBadSyntax = !wlCompiled.isValid(); decisionCache.clear(); } if (!Objects.equals(bl, blLast)) { blLast = bl; blCompiled = TagMatcher.compile(bl); + blacklistBadSyntax = !blCompiled.isValid(); decisionCache.clear(); } @@ -176,6 +190,7 @@ private void ensureCompiledUpToDate() { protected boolean isAllowed(AEFluidKey key) { ensureCompiledUpToDate(); + if (whitelistBadSyntax || blacklistBadSyntax) return false; if ((wlLast == null || wlLast.isEmpty()) && (blLast == null || blLast.isEmpty())) return false; @@ -367,7 +382,8 @@ public boolean mouseWheelMove(double mouseX, double mouseY, double wheelDelta) { 7, y, 160, 25, () -> Wltmp, v -> { Wltmp = v; }, - Component.literal("Whitelist tags...")); + Component.literal("Whitelist tags..."), + () -> whitelistBadSyntax ? 0xFFFF0000 : null); group.addWidget(WLField); y += 29; @@ -375,7 +391,8 @@ public boolean mouseWheelMove(double mouseX, double mouseY, double wheelDelta) { 7, y, 160, 25, () -> Bltmp, v -> { Bltmp = v; }, - Component.literal("Blacklist tags...")); + Component.literal("Blacklist tags..."), + () -> blacklistBadSyntax ? 0xFFFF0000 : null); group.addWidget(BLField); WLField.setDirectly(whitelistExpr); diff --git a/src/main/java/net/neganote/gtutilities/integration/ae2/machine/METagStockingInputBusPartMachine.java b/src/main/java/net/neganote/gtutilities/integration/ae2/machine/METagStockingInputBusPartMachine.java index 9eda414..8e2ba70 100644 --- a/src/main/java/net/neganote/gtutilities/integration/ae2/machine/METagStockingInputBusPartMachine.java +++ b/src/main/java/net/neganote/gtutilities/integration/ae2/machine/METagStockingInputBusPartMachine.java @@ -77,6 +77,12 @@ public class METagStockingInputBusPartMachine extends MEStockingBusPartMachine { @DescSynced protected String Bltmp = ""; + @DescSynced + protected boolean whitelistBadSyntax = false; + + @DescSynced + protected boolean blacklistBadSyntax = false; + private Predicate tagAutoPullTest = ($) -> true; private transient String wlLast = null; @@ -159,11 +165,13 @@ private void ensureCompiledUpToDate() { if (!Objects.equals(wl, wlLast)) { wlLast = wl; wlCompiled = TagMatcher.compile(wl); + whitelistBadSyntax = !wlCompiled.isValid(); decisionCache.clear(); } if (!Objects.equals(bl, blLast)) { blLast = bl; blCompiled = TagMatcher.compile(bl); + blacklistBadSyntax = !blCompiled.isValid(); decisionCache.clear(); } @@ -174,6 +182,7 @@ private void ensureCompiledUpToDate() { protected boolean isAllowed(AEItemKey key) { ensureCompiledUpToDate(); + if (whitelistBadSyntax || blacklistBadSyntax) return false; if ((wlLast == null || wlLast.isEmpty()) && (blLast == null || blLast.isEmpty())) return false; @@ -283,7 +292,8 @@ public Widget createUIWidget() { v -> { Wltmp = v; }, - Component.literal("Whitelist tags...")); + Component.literal("Whitelist tags..."), + () -> whitelistBadSyntax ? 0xFFFF0000 : null); group.addWidget(WLField); y += 29; @@ -293,7 +303,8 @@ public Widget createUIWidget() { v -> { Bltmp = v; }, - Component.literal("Blacklist tags...")); + Component.literal("Blacklist tags..."), + () -> blacklistBadSyntax ? 0xFFFF0000 : null); group.addWidget(BLField); WLField.setDirectly(whitelistExpr); diff --git a/src/main/java/net/neganote/gtutilities/integration/ae2/machine/METagStockingInputHatchPartMachine.java b/src/main/java/net/neganote/gtutilities/integration/ae2/machine/METagStockingInputHatchPartMachine.java index 3f8ea55..fac6d15 100644 --- a/src/main/java/net/neganote/gtutilities/integration/ae2/machine/METagStockingInputHatchPartMachine.java +++ b/src/main/java/net/neganote/gtutilities/integration/ae2/machine/METagStockingInputHatchPartMachine.java @@ -13,7 +13,9 @@ import com.gregtechceu.gtceu.integration.ae2.gui.widget.AEFluidConfigWidget; import com.gregtechceu.gtceu.integration.ae2.machine.MEHatchPartMachine; import com.gregtechceu.gtceu.integration.ae2.machine.MEStockingHatchPartMachine; -import com.gregtechceu.gtceu.integration.ae2.slot.*; +import com.gregtechceu.gtceu.integration.ae2.slot.ExportOnlyAEFluidList; +import com.gregtechceu.gtceu.integration.ae2.slot.ExportOnlyAEFluidSlot; +import com.gregtechceu.gtceu.integration.ae2.slot.ExportOnlyAESlot; import com.gregtechceu.gtceu.integration.ae2.utils.AEUtil; import com.lowdragmc.lowdraglib.gui.texture.GuiTextureGroup; @@ -75,6 +77,12 @@ public class METagStockingInputHatchPartMachine extends MEStockingHatchPartMachi @DescSynced protected String Bltmp = ""; + @DescSynced + protected boolean whitelistBadSyntax = false; + + @DescSynced + protected boolean blacklistBadSyntax = false; + private Predicate tagAutoPullTest = ($) -> true; private transient String wlLast = null; @@ -157,11 +165,13 @@ private void ensureCompiledUpToDate() { if (!Objects.equals(wl, wlLast)) { wlLast = wl; wlCompiled = TagMatcher.compile(wl); + whitelistBadSyntax = !wlCompiled.isValid(); decisionCache.clear(); } if (!Objects.equals(bl, blLast)) { blLast = bl; blCompiled = TagMatcher.compile(bl); + blacklistBadSyntax = !wlCompiled.isValid(); decisionCache.clear(); } @@ -172,6 +182,7 @@ private void ensureCompiledUpToDate() { protected boolean isAllowed(AEFluidKey key) { ensureCompiledUpToDate(); + if (whitelistBadSyntax || blacklistBadSyntax) return false; if ((wlLast == null || wlLast.isEmpty()) && (blLast == null || blLast.isEmpty())) return false; @@ -282,7 +293,8 @@ public Widget createUIWidget() { v -> { Wltmp = v; }, - Component.literal("Whitelist tags...")); + Component.literal("Whitelist tags..."), + () -> whitelistBadSyntax ? 0xFFFF0000 : null); group.addWidget(WLField); y += 29; @@ -292,7 +304,8 @@ public Widget createUIWidget() { v -> { Bltmp = v; }, - Component.literal("Blacklist tags...")); + Component.literal("Blacklist tags..."), + () -> blacklistBadSyntax ? 0xFFFF0000 : null); group.addWidget(BLField); WLField.setDirectly(whitelistExpr); diff --git a/src/main/java/net/neganote/gtutilities/utils/TagMatcher.java b/src/main/java/net/neganote/gtutilities/utils/TagMatcher.java index 4d81269..ce3f040 100644 --- a/src/main/java/net/neganote/gtutilities/utils/TagMatcher.java +++ b/src/main/java/net/neganote/gtutilities/utils/TagMatcher.java @@ -20,6 +20,13 @@ public final class TagMatcher { + private static final class InvalidTagMatcherSyntaxException extends Exception { + + private InvalidTagMatcherSyntaxException(String s) { + super(s); + } + } + public static Compiled compile(@Nullable String expr) { String washed = washExpression(expr == null ? "" : expr); if (washed.isBlank()) return Compiled.EMPTY; @@ -37,7 +44,7 @@ public static Compiled compile(@Nullable String expr) { } return new Compiled(rpn, true, needsTags); - } catch (RuntimeException ex) { + } catch (InvalidTagMatcherSyntaxException ex) { return Compiled.INVALID; } } @@ -53,7 +60,11 @@ public static boolean doesItemMatch(@Nullable AEItemKey item, @Nullable Compiled if (item == null || compiled == null || !compiled.isValid()) return false; if (!compiled.needsTags()) return false; // no tags referenced => cannot match anything meaningful ItemTagCache cache = ItemTagCache.of(item); - return evalRpn(compiled.rpn, cache.tags); + try { + return evalRpn(compiled.rpn, cache.tags); + } catch (InvalidTagMatcherSyntaxException e) { + return false; + } } public static boolean doesFluidMatch(@Nullable AEFluidKey fluid, String expr) { @@ -67,7 +78,11 @@ public static boolean doesFluidMatch(@Nullable AEFluidKey fluid, @Nullable Compi if (fluid == null || compiled == null || !compiled.isValid()) return false; if (!compiled.needsTags()) return false; FluidTagCache cache = FluidTagCache.of(fluid); - return evalRpn(compiled.rpn, cache.tags); + try { + return evalRpn(compiled.rpn, cache.tags); + } catch (InvalidTagMatcherSyntaxException e) { + return false; + } } private static String washExpression(String expression) { @@ -210,7 +225,7 @@ static Token rparen() { } } - private static List tokenize(String expression) { + private static List tokenize(String expression) throws InvalidTagMatcherSyntaxException { List tokens = new ArrayList<>(); StringBuilder currentTag = new StringBuilder(); @@ -222,21 +237,23 @@ private static List tokenize(String expression) { char c = expression.charAt(i); if (c == '#') { - throw new IllegalArgumentException("Character '#' is not allowed in tag expressions (pos " + i + ")."); + throw new InvalidTagMatcherSyntaxException( + "Character '#' is not allowed in tag expressions (pos " + i + ")."); } if (Character.isWhitespace(c)) continue; Operator op = Operator.fromSymbol(c); if (c == '(') { - if (!expectingOperand) throw new IllegalArgumentException("Unexpected '(' at position " + i); + if (!expectingOperand) throw new InvalidTagMatcherSyntaxException("Unexpected '(' at position " + i); flushTag(currentTag, tokens); tokens.add(Token.lparen()); lp++; lastIsTag = false; } else if (c == ')') { - if (expectingOperand && lp <= 0) throw new IllegalArgumentException("Unexpected ')' at position " + i); + if (expectingOperand && lp <= 0) + throw new InvalidTagMatcherSyntaxException("Unexpected ')' at position " + i); flushTag(currentTag, tokens); tokens.add(Token.rparen()); expectingOperand = false; @@ -254,13 +271,13 @@ private static List tokenize(String expression) { expectingOperand = true; } } else { - throw new IllegalArgumentException("Unexpected operator '" + c + "' at position " + i); + throw new InvalidTagMatcherSyntaxException("Unexpected operator '" + c + "' at position " + i); } lastIsTag = false; } else { if (!expectingOperand) - throw new IllegalArgumentException("Unexpected character '" + c + "' at position " + i); + throw new InvalidTagMatcherSyntaxException("Unexpected character '" + c + "' at position " + i); currentTag.append(c); lastIsTag = true; } @@ -268,12 +285,12 @@ private static List tokenize(String expression) { flushTag(currentTag, tokens); - if (tokens.isEmpty()) throw new IllegalArgumentException("Expression cannot be empty."); - if (lp > 0) throw new IllegalArgumentException("Missing ')' at the end of the expression."); + if (tokens.isEmpty()) throw new InvalidTagMatcherSyntaxException("Expression cannot be empty."); + if (lp > 0) throw new InvalidTagMatcherSyntaxException("Missing ')' at the end of the expression."); Token last = tokens.get(tokens.size() - 1); if (expectingOperand && last.type != TokenType.TAG && last.type != TokenType.RPAREN) { - throw new IllegalArgumentException("Expression ended unexpectedly."); + throw new InvalidTagMatcherSyntaxException("Expression ended unexpectedly."); } return tokens; @@ -286,7 +303,7 @@ private static void flushTag(StringBuilder currentTag, List tokens) { } } - private static Token[] toRpn(List tokens) { + private static Token[] toRpn(List tokens) throws InvalidTagMatcherSyntaxException { ArrayList out = new ArrayList<>(tokens.size()); Deque stack = new ArrayDeque<>(); @@ -321,21 +338,48 @@ private static Token[] toRpn(List tokens) { } out.add(stack.pop()); } - if (!found) throw new IllegalArgumentException("Mismatched parentheses."); + if (!found) throw new InvalidTagMatcherSyntaxException("Mismatched parentheses."); } } } while (!stack.isEmpty()) { Token top = stack.pop(); - if (top.type == TokenType.LPAREN) throw new IllegalArgumentException("Mismatched parentheses."); + if (top.type == TokenType.LPAREN) throw new InvalidTagMatcherSyntaxException("Mismatched parentheses."); out.add(top); } - return out.toArray(Token[]::new); + Token[] rpn = out.toArray(Token[]::new); + validateRpnStackDepth(rpn); + return rpn; + } + + private static void validateRpnStackDepth(Token[] rpn) throws InvalidTagMatcherSyntaxException { + int sp = 0; + for (Token t : rpn) { + switch (t.type) { + case TAG: + sp++; + break; + case OPERATOR: + int requiredDepth = t.op == Operator.NOT ? 1 : 2; + sp -= requiredDepth; + if (sp < 0) { + throw new InvalidTagMatcherSyntaxException("Unexpected operator " + t.op); + } + sp++; + break; + case LPAREN: + case RPAREN: + throw new InvalidTagMatcherSyntaxException("Unexpected token: " + t.type); + } + } + if (sp != 1) { + throw new InvalidTagMatcherSyntaxException("Depth at the end should equal 1"); + } } - private static boolean evalRpn(Token[] rpn, Set actualTags) { + private static boolean evalRpn(Token[] rpn, Set actualTags) throws InvalidTagMatcherSyntaxException { if (rpn.length == 0) return false; boolean[] stack = new boolean[rpn.length]; @@ -355,28 +399,28 @@ private static boolean evalRpn(Token[] rpn, Set actualTags) { Operator op = t.op; if (op == Operator.NOT) { - if (sp < 1) throw new IllegalArgumentException("NOT needs 1 operand."); + if (sp < 1) throw new InvalidTagMatcherSyntaxException("NOT needs 1 operand."); stack[sp - 1] = !stack[sp - 1]; } else { - if (sp < 2) throw new IllegalArgumentException(op.symbol + " needs 2 operands."); + if (sp < 2) throw new InvalidTagMatcherSyntaxException(op.symbol + " needs 2 operands."); boolean right = stack[--sp]; boolean left = stack[--sp]; boolean res = switch (op) { case AND -> left && right; case OR -> left || right; case XOR -> left ^ right; - default -> throw new IllegalStateException("Unexpected op: " + op); + default -> throw new InvalidTagMatcherSyntaxException("Unexpected op: " + op); }; stack[sp++] = res; } } else { - throw new IllegalStateException("Paren token in RPN (should not happen)."); + throw new InvalidTagMatcherSyntaxException("Paren token in RPN (should not happen)."); } } if (sp == 1) return stack[0]; - throw new IllegalArgumentException("Invalid expression: stack size " + sp); + throw new InvalidTagMatcherSyntaxException("Invalid expression: stack size " + sp); } private static boolean matchesAnyGlob(String pattern, Set tags) {