diff --git a/src/main/java/rs117/hd/renderer/legacy/LegacyModelPusher.java b/src/main/java/rs117/hd/renderer/legacy/LegacyModelPusher.java index 36d0c6996d..a360495fb1 100644 --- a/src/main/java/rs117/hd/renderer/legacy/LegacyModelPusher.java +++ b/src/main/java/rs117/hd/renderer/legacy/LegacyModelPusher.java @@ -337,12 +337,10 @@ public void pushModel( } else if (modelOverride.colorOverrides != null && (cacheUvData || !needsCaching)) { // Color overrides are heavy. Only apply them if the UVs will be cached or don't need caching int ahsl = (faceTransparencies == null ? 0xFF : 0xFF - (faceTransparencies[face] & 0xFF)) << 16 | faceColors[face]; - for (var override : modelOverride.colorOverrides) { - if (override.ahslCondition.test(ahsl)) { - faceOverride = override; - material = faceOverride.baseMaterial; - break; - } + final var override = modelOverride.testColorOverrides(ahsl); + if (override != null) { + faceOverride = override; + material = faceOverride.baseMaterial; } } diff --git a/src/main/java/rs117/hd/renderer/zone/SceneUploader.java b/src/main/java/rs117/hd/renderer/zone/SceneUploader.java index 9716915bbd..9c052bb5e7 100644 --- a/src/main/java/rs117/hd/renderer/zone/SceneUploader.java +++ b/src/main/java/rs117/hd/renderer/zone/SceneUploader.java @@ -37,6 +37,7 @@ import rs117.hd.scene.SceneContext; import rs117.hd.scene.ground_materials.GroundMaterial; import rs117.hd.scene.materials.Material; +import rs117.hd.scene.model_overrides.AhslSupplier; import rs117.hd.scene.model_overrides.InheritTileColorType; import rs117.hd.scene.model_overrides.ModelOverride; import rs117.hd.scene.model_overrides.TzHaarRecolorType; @@ -147,6 +148,8 @@ public interface OnBeforeProcessTileFunc { private final GpuIntBuffer zoneVboA = new GpuIntBuffer(false); private final GpuIntBuffer zoneTboF = new GpuIntBuffer(false); + private final AhslSupplier ahslSupplier = new AhslSupplier(); + // Lazily initialized staging buffers public VertexWriteCache.Collection writeCache; @@ -1631,8 +1634,7 @@ private int uploadStaticModel( } } } else if (modelOverride.colorOverrides != null) { - final int ahsl = (0xFF - transparency) << 16 | color1; - final var override = modelOverride.testColorOverrides(ahsl); + final var override = modelOverride.testColorOverrides(ahslSupplier.ahsl(transparency, color1)); if (override != null) { faceOverride = override; material = faceOverride.baseMaterial; @@ -1972,6 +1974,7 @@ public boolean preprocessTempModel( visibleFaces.ensureCapacity(triangleCount); culledFaces.ensureCapacity(triangleCount); + final int[] color1s = model.getFaceColors1(); final int[] color3s = model.getFaceColors3(); final int[] indices1 = model.getFaceIndices1(); final int[] indices2 = model.getFaceIndices2(); @@ -2029,8 +2032,7 @@ public boolean preprocessTempModel( } } } else if (modelOverride.colorOverrides != null) { - final int ahsl = (0xFF - transparency) << 16 | model.getFaceColors1()[f]; - final var override = modelOverride.testColorOverrides(ahsl); + final var override = modelOverride.testColorOverrides(ahslSupplier.ahsl(transparency, color1s[f])); if (override != null) { faceOverride = override; material = faceOverride.baseMaterial; diff --git a/src/main/java/rs117/hd/scene/ProceduralGenerator.java b/src/main/java/rs117/hd/scene/ProceduralGenerator.java index d9bed27adc..590ede8f3b 100644 --- a/src/main/java/rs117/hd/scene/ProceduralGenerator.java +++ b/src/main/java/rs117/hd/scene/ProceduralGenerator.java @@ -34,6 +34,7 @@ import rs117.hd.scene.model_overrides.ModelOverride; import rs117.hd.scene.model_overrides.TzHaarRecolorType; import rs117.hd.scene.tile_overrides.TileOverride; +import rs117.hd.scene.tile_overrides.TileOverrideVariables; import rs117.hd.scene.water_types.WaterType; import rs117.hd.utils.ColorUtils; import rs117.hd.utils.collections.ConcurrentPool; @@ -84,6 +85,7 @@ public class ProceduralGenerator { @Inject private WaterTypeManager waterTypeManager; + private final TileOverrideVariables tileVar = new TileOverrideVariables(); private final ConcurrentPool GENERATOR_POOL = new ConcurrentPool<>(GeneratorContext::new); final class GeneratorContext implements AutoCloseable { @@ -555,9 +557,13 @@ private void calculateTileOverride( sceneContext.extendedSceneToWorld(tileExX, tileExY, tileZ, worldPos); - overrides[0] = tileOverrideManager.getOverride(sceneContext, tile, worldPos, ids); - overrides[1] = tileOverrideManager.getOverride(sceneContext, tile, worldPos, ids[1]); - overrides[2] = tileOverrideManager.getOverride(sceneContext, tile, worldPos, ids[0]); + try { + overrides[0] = tileOverrideManager.getOverride(sceneContext, tileVar, worldPos, ids); + overrides[1] = tileOverrideManager.getOverride(sceneContext, tileVar, worldPos, ids[1]); + overrides[2] = tileOverrideManager.getOverride(sceneContext, tileVar, worldPos, ids[0]); + } finally { + tileVar.setTile(null); + } sceneContext.setTileOverride(tileZ, tileExX, tileExY, overrides); } diff --git a/src/main/java/rs117/hd/scene/TileOverrideManager.java b/src/main/java/rs117/hd/scene/TileOverrideManager.java index d8280ba615..c72281aa54 100644 --- a/src/main/java/rs117/hd/scene/TileOverrideManager.java +++ b/src/main/java/rs117/hd/scene/TileOverrideManager.java @@ -20,6 +20,7 @@ import rs117.hd.scene.areas.Area; import rs117.hd.scene.ground_materials.GroundMaterial; import rs117.hd.scene.tile_overrides.TileOverride; +import rs117.hd.scene.tile_overrides.TileOverrideVariables; import rs117.hd.utils.FileWatcher; import rs117.hd.utils.Props; import rs117.hd.utils.ResourcePath; @@ -228,6 +229,17 @@ public TileOverride getOverride(SceneContext sceneContext, Tile tile) { @Nonnull public TileOverride getOverride(SceneContext sceneContext, @Nonnull Tile tile, @Nonnull int[] worldPos, int... ids) { + final var vars = SceneContext.TILE_OVERRIDE_VARIABLES.get(); + try { + return getOverride(sceneContext, vars.setTile(tile), worldPos, ids); + } finally { + vars.setTile(null); // Avoid accidentally keeping the old scene in memory + } + } + + @Nonnull + public TileOverride getOverride(SceneContext sceneContext, TileOverrideVariables vars, @Nonnull int[] worldPos, int... ids) { + final Tile tile = vars.getTile(); if (ids.length == 0) { var pos = tile.getSceneLocation(); int x = pos.getX() + sceneContext.sceneOffset; @@ -244,11 +256,7 @@ public TileOverride getOverride(SceneContext sceneContext, @Nonnull Tile tile, @ if (override.isConstant()) return override; - final var vars = SceneContext.TILE_OVERRIDE_VARIABLES.get(); - vars.setTile(tile); - TileOverride replacement = override.resolveReplacements(vars); - vars.setTile(null); // Avoid accidentally keeping the old scene in memory - return replacement; + return override.resolveReplacements(vars); } @Nonnull diff --git a/src/main/java/rs117/hd/scene/model_overrides/AhslSupplier.java b/src/main/java/rs117/hd/scene/model_overrides/AhslSupplier.java new file mode 100644 index 0000000000..75562b79f9 --- /dev/null +++ b/src/main/java/rs117/hd/scene/model_overrides/AhslSupplier.java @@ -0,0 +1,68 @@ +package rs117.hd.scene.model_overrides; + +import rs117.hd.utils.VariableSupplier; + +public final class AhslSupplier implements VariableSupplier { + private int alpha; + private int h, s, l; + private int ahsl, hsl; + + public AhslSupplier ahsl(int ahsl) { + this.ahsl = ahsl; + alpha = (ahsl >> 16) & 0xFF; + hsl = ahsl & 0xFFFF; + h = ahsl >>> 10 & 0x3F; + s = ahsl >>> 7 & 0x7; + l = ahsl & 0x7F; + return this; + } + + public AhslSupplier ahsl(int transparency, int color) { + ahsl = (0xFF - transparency) << 16 | hsl; + alpha = transparency & 0xFF; + hsl = color & 0xFFFF; + h = ahsl >>> 10 & 0x3F; + s = ahsl >>> 7 & 0x7; + l = ahsl & 0x7F; + return this; + } + + @Override + public Object get(String name) { + return getInt(name); + } + + @Override + public int getInt(String name) { + if (name.length() == 1) { + switch (name.charAt(0)) { + case 'a': + return alpha; + case 'h': + return h; + case 's': + return s; + case 'l': + return l; + default: + assert false : "Unexpected variable: " + name; + return 0; + } + } + + switch (name) { + case "ahsl": + return ahsl; + case "hsl": + return hsl; + default: + assert false : "Unexpected variable: " + name; + return 0; + } + } + + @Override + public float getFloat(String name) { + return getInt(name); + } +} diff --git a/src/main/java/rs117/hd/scene/model_overrides/HslComparison.java b/src/main/java/rs117/hd/scene/model_overrides/HslComparison.java new file mode 100644 index 0000000000..07ff52e05a --- /dev/null +++ b/src/main/java/rs117/hd/scene/model_overrides/HslComparison.java @@ -0,0 +1,13 @@ +package rs117.hd.scene.model_overrides; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public final class HslComparison implements ModelOverride.AhslPredicate { + public final int targetHsl; + + @Override + public boolean test(AhslSupplier vars) { + return vars.getInt("hsl") == targetHsl; + } +} diff --git a/src/main/java/rs117/hd/scene/model_overrides/ModelOverride.java b/src/main/java/rs117/hd/scene/model_overrides/ModelOverride.java index a464c3bcc8..6bca6c1470 100644 --- a/src/main/java/rs117/hd/scene/model_overrides/ModelOverride.java +++ b/src/main/java/rs117/hd/scene/model_overrides/ModelOverride.java @@ -3,13 +3,17 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.annotations.JsonAdapter; +import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Nullable; import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.experimental.Accessors; import lombok.extern.slf4j.Slf4j; @@ -34,11 +38,22 @@ @AllArgsConstructor public class ModelOverride { + private static final ThreadLocal LOCAL_AHSL_SUPPLIER = ThreadLocal.withInitial(AhslSupplier::new); + public static final ModelOverride NONE = new ModelOverride(true); public static final ModelOverride UNLIT = new ModelOverride(true).baseMaterial(Material.UNLIT).undoVanillaShading(false); private static final Set EMPTY = new HashSet<>(); + private static final Comparator WEIGHTED_PREDICATE_COMPARATOR = Comparator.comparingInt(v -> v.weight); + private static final List PREDICATES = new ArrayList<>(); + + @RequiredArgsConstructor + private static class WeightedPredicate { + public final AhslPredicate condition; + public final int weight; + } + public String description = "UNKNOWN"; // When, where or what the override should apply to @@ -123,12 +138,9 @@ public class ModelOverride public transient boolean mightBeDoubleSided; public transient boolean modifiesVanillaTexture; - // Transient not volatile, since access order can be random as it'll mean we'll just fall back to the full lookup - private transient long cachedColorOverrideAhsl = -1; - @FunctionalInterface public interface AhslPredicate { - boolean test(int ahsl); + boolean test(AhslSupplier vars); } public void normalize(HdPlugin plugin) { @@ -349,9 +361,7 @@ public ModelOverride copy() { ahslCondition, mightHaveTransparency, mightBeDoubleSided, - modifiesVanillaTexture, - // Runtime caching fields - -1 + modifiesVanillaTexture ); } @@ -372,8 +382,7 @@ private AhslPredicate parseAhslConditions(JsonElement element) { arr.add(element); } - AhslPredicate combinedPredicate = null; - + PREDICATES.clear(); for (var el : arr) { if (el.isJsonNull()) continue; @@ -382,21 +391,21 @@ private AhslPredicate parseAhslConditions(JsonElement element) { continue; } - AhslPredicate condition; + AhslPredicate condition = null; + int weight = 1; var prim = el.getAsJsonPrimitive(); if (prim.isBoolean()) { - boolean bool = prim.getAsBoolean(); - condition = ahsl -> bool; + if (prim.getAsBoolean()) + condition = ahsl -> true; } else if (prim.isNumber()) { try { - int targetHsl = prim.getAsInt(); - condition = ahsl -> (ahsl & 0xFFFF) == targetHsl; + condition = new HslComparison(prim.getAsInt()); } catch (Exception ex) { log.warn("Expected integer, but got {} in override '{}'", el, description); continue; } } else if (prim.isString()) { - var expr = asExpression(parseExpression(prim.getAsString())); + final var expr = asExpression(parseExpression(prim.getAsString())); if (Props.DEVELOPMENT) { // Ensure all variables are defined @@ -407,43 +416,38 @@ private AhslPredicate parseAhslConditions(JsonElement element) { "Expression '" + prim.getAsString() + "' contains unknown variable '" + variable + "'"); } - var predicate = expr.toPredicate(); - condition = ahsl -> predicate.test(key -> { - switch (key) { - case "a": - return ahsl >>> 16 & 0xFF; - case "h": - return ahsl >>> 10 & 0x3F; - case "s": - return ahsl >>> 7 & 0x7; - case "l": - return ahsl & 0x7F; - case "ahsl": - return ahsl; - case "hsl": - return ahsl & 0xFFFF; - default: - assert false : "Unexpected variable: " + key; - return 0; - } - }); + final var predicate = expr.toPredicate(); + condition = predicate::test; + weight = expr.variables.size(); } else { log.warn("Skipping unexpected HSL condition primitive '{}' in override '{}'", el, description); continue; } - if (combinedPredicate == null) { - combinedPredicate = condition; - } else { - var prev = combinedPredicate; - combinedPredicate = ahsl -> prev.test(ahsl) || condition.test(ahsl); - } + if (condition != null) + PREDICATES.add(new WeightedPredicate(condition, weight)); } - if (combinedPredicate == null) + if (PREDICATES.isEmpty()) return ahsl -> false; - return combinedPredicate; + final int conditionCount = PREDICATES.size(); + if (conditionCount == 1) + return PREDICATES.get(0).condition; + + // Sort based on weight to push cheaper conditions first + PREDICATES.sort(WEIGHTED_PREDICATE_COMPARATOR); + + final AhslPredicate[] conditions = new AhslPredicate[conditionCount]; + for (int i = 0; i < conditionCount; i++) + conditions[i] = PREDICATES.get(i).condition; + + return ahsl -> { + for (int i = 0; i < conditionCount; i++) + if (conditions[i].test(ahsl)) + return true; + return false; + }; } public void computeModelUvw(float[] out, int i, float x, float y, float z, int orientation) { @@ -734,23 +738,20 @@ public int modifyColor(int jagexHsl) { @Nullable public final ModelOverride testColorOverrides(int ahsl) { - ModelOverride override = null; - final long packedAhl = cachedColorOverrideAhsl; - if (packedAhl != -1 && ahsl == (int) packedAhl) - override = colorOverrides[(int) (packedAhl >> 32)]; - - if (override == null) { - final int len = colorOverrides.length; - for (int i = 0; i < len; ++i) { - final var inner = colorOverrides[i]; - if (inner.ahslCondition.test(ahsl)) { - cachedColorOverrideAhsl = ahsl | (long) i << 32; - override = inner; - break; - } - } - } + if (colorOverrides.length == 0) + return null; - return override; + return testColorOverrides(LOCAL_AHSL_SUPPLIER.get().ahsl(ahsl)); + } + + @Nullable + public final ModelOverride testColorOverrides(AhslSupplier vars) { + final int len = colorOverrides.length; + for (int i = 0; i < len; ++i) { + final var override = colorOverrides[i]; + if (override.ahslCondition.test(vars)) + return override; + } + return null; } } diff --git a/src/main/java/rs117/hd/scene/tile_overrides/TileOverrideVariables.java b/src/main/java/rs117/hd/scene/tile_overrides/TileOverrideVariables.java index 6766afb4b4..130f322a0d 100644 --- a/src/main/java/rs117/hd/scene/tile_overrides/TileOverrideVariables.java +++ b/src/main/java/rs117/hd/scene/tile_overrides/TileOverrideVariables.java @@ -1,32 +1,55 @@ package rs117.hd.scene.tile_overrides; +import lombok.Getter; import net.runelite.api.*; import rs117.hd.utils.HDUtils; import rs117.hd.utils.VariableSupplier; public class TileOverrideVariables implements VariableSupplier { - private final String[] HSL_VARS = { "h", "s", "l" }; private final int[] hsl = new int[3]; + @Getter private Tile tile; private boolean requiresHslUpdate; - public void setTile(Tile tile) { + public TileOverrideVariables setTile(Tile tile) { if (tile == this.tile) - return; + return this; this.tile = tile; requiresHslUpdate = true; + return this; } @Override public Object get(String name) { - for (int i = 0; i < HSL_VARS.length; i++) { - if (HSL_VARS[i].equals(name)) { + return getInt(name); + } + + @Override + public int getInt(String name) { + if (name.length() == 1) { + final int idx; + switch (name.charAt(0)) { + case 'h': + idx = 0; + break; + case 's': + idx = 1; + break; + case 'l': + idx = 2; + break; + default: + idx = -1; + break; + } + + if (idx != -1) { if (requiresHslUpdate) { HDUtils.getSouthWesternMostTileColor(hsl, tile); requiresHslUpdate = false; } - return hsl[i]; + return hsl[idx]; } } diff --git a/src/main/java/rs117/hd/utils/ExpressionParser.java b/src/main/java/rs117/hd/utils/ExpressionParser.java index 1d3f0ffbbc..ff96d4b211 100644 --- a/src/main/java/rs117/hd/utils/ExpressionParser.java +++ b/src/main/java/rs117/hd/utils/ExpressionParser.java @@ -13,7 +13,29 @@ import lombok.AllArgsConstructor; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; - +import rs117.hd.utils.ExpressionParserEvaluators.BooleanComparisons; +import rs117.hd.utils.ExpressionParserEvaluators.BooleanConstant; +import rs117.hd.utils.ExpressionParserEvaluators.BooleanEvalPredicate; +import rs117.hd.utils.ExpressionParserEvaluators.BooleanNot; +import rs117.hd.utils.ExpressionParserEvaluators.BooleanTernary; +import rs117.hd.utils.ExpressionParserEvaluators.BooleanToObjectFunction; +import rs117.hd.utils.ExpressionParserEvaluators.BooleanVariable; +import rs117.hd.utils.ExpressionParserEvaluators.ConstantFunction; +import rs117.hd.utils.ExpressionParserEvaluators.FloatComparisons; +import rs117.hd.utils.ExpressionParserEvaluators.FloatConstant; +import rs117.hd.utils.ExpressionParserEvaluators.FloatTernary; +import rs117.hd.utils.ExpressionParserEvaluators.FloatToObjectFunction; +import rs117.hd.utils.ExpressionParserEvaluators.FloatVariable; +import rs117.hd.utils.ExpressionParserEvaluators.IntComparisons; +import rs117.hd.utils.ExpressionParserEvaluators.IntConstant; +import rs117.hd.utils.ExpressionParserEvaluators.IntTernary; +import rs117.hd.utils.ExpressionParserEvaluators.IntToObjectFunction; +import rs117.hd.utils.ExpressionParserEvaluators.IntVariable; +import rs117.hd.utils.ExpressionParserEvaluators.ObjectTernaryFunction; +import rs117.hd.utils.ExpressionParserEvaluators.ObjectVariableFunction; + +import static rs117.hd.utils.ExpressionParser.Operator.NOT; +import static rs117.hd.utils.ExpressionParser.Operator.TERNARY; import static rs117.hd.utils.MathUtils.*; public class ExpressionParser { @@ -82,10 +104,56 @@ public static Function asFunction(Object object) { if (object instanceof Expression) return ((Expression) object).toFunction(); if (object instanceof String) - return vars -> vars.get((String) object); - return vars -> object; + return new ObjectVariableFunction((String) object); + return new ConstantFunction(object); + } + + @FunctionalInterface + public interface IntEval { + int apply(VariableSupplier vars); + } + + @FunctionalInterface + public interface FloatEval { + float apply(VariableSupplier vars); + } + + @FunctionalInterface + public interface BooleanEval { + boolean apply(VariableSupplier vars); + } + + private static IntEval toIntEval(Object operand) { + if (operand instanceof Expression) + return ((Expression) operand).compileInt(); + if (operand instanceof Number) + return new IntConstant(((Number) operand).intValue()); + if (operand instanceof String) + return new IntVariable((String) operand); + throw new IllegalArgumentException("Cannot evaluate '" + operand + "' as a number"); + } + + private static FloatEval toFloatEval(Object operand) { + if (operand instanceof Expression) + return ((Expression) operand).compileFloat(); + if (operand instanceof Number) + return new FloatConstant(((Number) operand).floatValue()); + if (operand instanceof String) + return new FloatVariable((String) operand); + throw new IllegalArgumentException("Cannot evaluate '" + operand + "' as a number"); + } + + private static BooleanEval toBooleanEval(Object operand) { + if (operand instanceof Expression) + return ((Expression) operand).compileBoolean(); + if (operand instanceof Boolean) + return new BooleanConstant((Boolean) operand); + if (operand instanceof String) + return new BooleanVariable((String) operand); + throw new IllegalArgumentException("Cannot evaluate '" + operand + "' as a boolean"); } + @RequiredArgsConstructor public static class SerializableExpressionPredicate implements ExpressionPredicate { public final Expression expression; public final ExpressionPredicate predicate; @@ -169,7 +237,7 @@ public static class SyntaxError extends IllegalArgumentException { } @RequiredArgsConstructor - private enum Operator { + public enum Operator { MOD("%", 6, 2), MUL("*", 6, 2), DIV("/", 6, 2), @@ -207,6 +275,7 @@ public static class ParserContext { Object[] operands = new Object[2]; boolean isInParentheses; boolean isTopLevelParser; + boolean hasDecimal; int minPrecedence; ParserContext(String expression, int startIndex, int endIndex, boolean isTopLevelParser, int minPrecedence) { @@ -346,8 +415,14 @@ Object parseOperand() { } } - if (c == '+' || c == '-' || c == '.' || ('0' <= c && c <= '9')) - return readNumber(); + if (c == '+' || c == '-' || c == '.' || ('0' <= c && c <= '9')) { + final float floatVal = readNumber(); + final int intVal = (int) floatVal; + hasDecimal = floatVal != intVal; + if (hasDecimal) + return floatVal; + return intVal; + } if ('A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || c == '_') return readIdentifier(); } @@ -436,14 +511,14 @@ Object readIdentifier() { Expression createExpression(Object leftOperand, Operator op, Object rightOperand) { if (!(leftOperand instanceof Expression)) { // Simple combination of left & right operands - return new Expression(op, leftOperand, rightOperand, null, false); + return new Expression(op, leftOperand, rightOperand, null, false, false); } Expression left = (Expression) leftOperand; // If the left expression is in parentheses, or has the same or higher operator precedence, // it should be evaluated first, so use it as the left operand in a new expression if (left.isInParentheses || left.op.precedence >= op.precedence) - return new Expression(op, left, rightOperand, null, false); + return new Expression(op, left, rightOperand, null, false, false); // The new operator should act on the left expression's right-most operand, // and should replace the right-most operand with the resulting expression @@ -457,18 +532,21 @@ public static class Expression { Object left, right; Object ternary; boolean isInParentheses; + boolean hasDecimal; + public final HashSet variables = new HashSet<>(); Expression(Object value) { - this(null, value, null, null, false); + this(null, value, null, null, false, false); } - Expression(Operator op, Object left, Object right, Object ternary, boolean isInParentheses) { + Expression(Operator op, Object left, Object right, Object ternary, boolean isInParentheses, boolean hasDecimal) { this.op = op; this.left = left; this.right = right; this.ternary = ternary; this.isInParentheses = isInParentheses; + this.hasDecimal = hasDecimal; registerVariables(left); registerVariables(right); registerVariables(ternary); @@ -482,34 +560,34 @@ public Object simplify(@Nullable VariableSupplier constants) { if (l instanceof String) { var value = constants.get((String) l); if (value != null) - l = sanitizeValue(value); + l = value; } if (r instanceof String) { var value = constants.get((String) r); if (value != null) - r = sanitizeValue(value); + r = value; } } - if (op == Operator.TERNARY) { + if (op == TERNARY) { Object t = asExpression(ternary).simplify(constants); if (t instanceof Boolean) return (boolean) t ? l : r; - return new Expression(op, l, r, asExpression(t), isInParentheses); + return new Expression(op, l, r, asExpression(t), isInParentheses, hasDecimal); } var expr = this; if (l != left || r != right) - expr = new Expression(op, l, r, null, isInParentheses); + expr = new Expression(op, l, r, null, isInParentheses, hasDecimal); if (isPrimitive(l) && isPrimitive(r)) - return expr.toFunctionInternal().apply(null); + return expr.toFunction().apply(null); return expr; } private String formatOperand(Object operand) { - if (operand instanceof Number) { + if (operand instanceof Float) { int nearest = round((float) operand); if (abs((float) operand - nearest) < 1e-10) operand = nearest; @@ -531,93 +609,76 @@ public String toString() { } public Function toFunction() { - var func = toFunctionInternal(); - return vars -> func.apply(key -> sanitizeValue(vars.get(key))); + if (op == null) + return asFunction(left); + if (op == TERNARY) + return compileObjectTernary(); + return isBoolean() ? + new BooleanToObjectFunction(compileBoolean()) : + hasDecimal ? new FloatToObjectFunction(compileFloat()) : new IntToObjectFunction(compileInt()); } - static Object sanitizeValue(Object value) { - // This is kind of stupid, but it's necessary to convert - // ints to floats here to avoid messy code later - if (value instanceof Integer) - return ((Integer) value).floatValue(); - return value; + private Function compileObjectTernary() { + BooleanEval condition = toBooleanEval(ternary); + Function ifTrue = asFunction(left); + Function ifFalse = asFunction(right); + return new ObjectTernaryFunction(condition, ifTrue, ifFalse); } - private Function toFunctionInternal() { + private IntEval compileInt() { if (op == null) - return asFunction(left); + return toIntEval(left); - if (op == Operator.TERNARY) { - var condition = asExpression(ternary).toPredicate(); - if (left instanceof Expression) { - var ifTrue = ((Expression) left).toFunction(); - if (right instanceof Expression) { - var ifFalse = ((Expression) right).toFunction(); - return vars -> condition.test(vars) ? ifTrue.apply(vars) : ifFalse.apply(vars); - } - return vars -> condition.test(vars) ? ifTrue.apply(vars) : right; - } else if (right instanceof Expression) { - var ifFalse = ((Expression) right).toFunction(); - return vars -> condition.test(vars) ? left : ifFalse.apply(vars); - } else { - return vars -> condition.test(vars) ? left : right; - } - } + if (op == TERNARY) + return new IntTernary(toBooleanEval(ternary), toIntEval(left), toIntEval(right)); - // Convert variables and constants into functions - var l = asFunction(left); - var r = asFunction(right); + return new ExpressionParserEvaluators.IntMathOperation(op, toIntEval(left), toIntEval(right)); + } - switch (op) { - case AND: - return vars -> (boolean) l.apply(vars) && (boolean) r.apply(vars); - case OR: - return vars -> (boolean) l.apply(vars) || (boolean) r.apply(vars); - case NOTEQUAL: - case EQUAL: - boolean isBoolean = - left instanceof Boolean || left instanceof Expression && ((Expression) left).isBoolean() || - right instanceof Boolean || right instanceof Expression && ((Expression) right).isBoolean(); - if (isBoolean) { - return op == Operator.EQUAL ? - vars -> (boolean) l.apply(vars) == (boolean) r.apply(vars) : - vars -> (boolean) l.apply(vars) != (boolean) r.apply(vars); - } else { - return op == Operator.EQUAL ? - vars -> (float) l.apply(vars) == (float) r.apply(vars) : - vars -> (float) l.apply(vars) != (float) r.apply(vars); - } - case GEQUAL: - return vars -> (float) l.apply(vars) >= (float) r.apply(vars); - case GREATER: - return vars -> (float) l.apply(vars) > (float) r.apply(vars); - case LEQUAL: - return vars -> (float) l.apply(vars) <= (float) r.apply(vars); - case LESS: - return vars -> (float) l.apply(vars) < (float) r.apply(vars); - case ADD: - return vars -> (float) l.apply(vars) + (float) r.apply(vars); - case SUB: - return vars -> (float) l.apply(vars) - (float) r.apply(vars); - case MUL: - return vars -> (float) l.apply(vars) * (float) r.apply(vars); - case DIV: - return vars -> (float) l.apply(vars) / (float) r.apply(vars); - case MOD: - return vars -> (float) l.apply(vars) % (float) r.apply(vars); - case NOT: - return vars -> !(boolean) r.apply(vars); + private FloatEval compileFloat() { + if (op == null) + return toFloatEval(left); + + if (op == TERNARY) + return new FloatTernary(toBooleanEval(ternary), toFloatEval(left), toFloatEval(right)); + + return new ExpressionParserEvaluators.FloatMathOperation(op, toFloatEval(left), toFloatEval(right)); + } + + private BooleanEval compileBoolean() { + if (op == null) + return toBooleanEval(left); + + if (op == NOT) + return new BooleanNot(toBooleanEval(right)); + + final boolean isFloatCompare = + left instanceof Float || left instanceof Expression && ((Expression) left).hasDecimal || + right instanceof Float || right instanceof Expression && ((Expression)right).hasDecimal; + + final boolean isBooleanCompare = + op == Operator.AND || op == Operator.OR || + left instanceof Boolean || left instanceof Expression && ((Expression) left).isBoolean() || + right instanceof Boolean || right instanceof Expression && ((Expression) right).isBoolean(); + + if (isBooleanCompare) { + if (op == TERNARY) + return new BooleanTernary(toBooleanEval(ternary), toBooleanEval(left), toBooleanEval(right)); + + return new BooleanComparisons(op, toBooleanEval(left), toBooleanEval(right)); } - throw new UnsupportedOperationException("Unsupported operands: " + l + " " + op + " " + r); + if (isFloatCompare) + return new FloatComparisons(op, toFloatEval(left), toFloatEval(right)); + + return new IntComparisons(op, toIntEval(left), toIntEval(right)); } public ExpressionPredicate toPredicate() { if (!isBoolean()) throw new IllegalArgumentException("Expression does not result in a boolean"); - var func = toFunction(); - return vars -> (boolean) func.apply(vars); + return new BooleanEvalPredicate(compileBoolean()); } boolean isBoolean() { @@ -648,7 +709,7 @@ static boolean isPossiblyBoolean(Object obj) { } private boolean isPrimitive(Object obj) { - return obj == null || obj instanceof Float || obj instanceof Boolean; + return obj == null || obj instanceof Integer || obj instanceof Float || obj instanceof Boolean; } private void registerVariables(@Nullable Object dependency) { @@ -672,8 +733,10 @@ private static Object parseExpression(ParserContext ctx) { ctx.trimParentheses(); boolean wasInParentheses = ctx.isInParentheses; boolean wasTopLevelParser = ctx.isTopLevelParser; + boolean didHaveDecimal = ctx.hasDecimal; // Since we'll be reusing the same parser context for parsing sub-expressions, mark it as not top-level ctx.isTopLevelParser = false; + ctx.hasDecimal = false; // The general gist: // 1. Begin parsing from left to right until any operator is reached @@ -691,7 +754,7 @@ private static Object parseExpression(ParserContext ctx) { for (var op : Operator.OPERATORS) { // Skip lower precedence operators if (op.precedence >= ctx.minPrecedence && ctx.expr.startsWith(op.symbol, ctx.index)) { - if (op == Operator.TERNARY) { + if (op == TERNARY) { // Parse the ternary into an expression to be the new left operand, and keep parsing var condition = ctx.operands[0]; if (condition == null) @@ -703,7 +766,7 @@ private static Object parseExpression(ParserContext ctx) { throw new SyntaxError(ctx, "Expected ':' in ternary expression"); ctx.advance(); var ifFalse = parseExpression(ctx); - ctx.operands[0] = new Expression(op, ifTrue, ifFalse, condition, wasInParentheses); + ctx.operands[0] = new Expression(op, ifTrue, ifFalse, condition, wasInParentheses, didHaveDecimal); continue parsing; } @@ -737,8 +800,11 @@ private static Object parseExpression(ParserContext ctx) { if (wasTopLevelParser && !ctx.done()) throw new SyntaxError(ctx, "Unexpected character '" + ctx.c + "'"); - if (ctx.operands[0] instanceof Expression) - ((Expression) ctx.operands[0]).isInParentheses = wasInParentheses; + if (ctx.operands[0] instanceof Expression) { + Expression expression = (Expression) ctx.operands[0]; + expression.isInParentheses = wasInParentheses; + expression.hasDecimal = didHaveDecimal; + } return ctx.operands[0]; } diff --git a/src/main/java/rs117/hd/utils/ExpressionParserEvaluators.java b/src/main/java/rs117/hd/utils/ExpressionParserEvaluators.java new file mode 100644 index 0000000000..86e2644eb4 --- /dev/null +++ b/src/main/java/rs117/hd/utils/ExpressionParserEvaluators.java @@ -0,0 +1,283 @@ +package rs117.hd.utils; + +import java.util.function.Function; +import lombok.RequiredArgsConstructor; +import rs117.hd.utils.ExpressionParser.BooleanEval; +import rs117.hd.utils.ExpressionParser.FloatEval; +import rs117.hd.utils.ExpressionParser.IntEval; +import rs117.hd.utils.ExpressionParser.Operator; + +public class ExpressionParserEvaluators { + @RequiredArgsConstructor + public static final class ConstantFunction implements Function { + private final Object value; + + @Override + public Object apply(VariableSupplier vars) { return value; } + } + + @RequiredArgsConstructor + public static final class ObjectVariableFunction implements Function { + private final String key; + + @Override + public Object apply(VariableSupplier vars) { return vars.get(key); } + } + + @RequiredArgsConstructor + public static final class IntToObjectFunction implements Function { + private final IntEval eval; + + @Override + public Object apply(VariableSupplier vars) { return eval.apply(vars); } + } + + @RequiredArgsConstructor + public static final class FloatToObjectFunction implements Function { + private final FloatEval eval; + + @Override + public Object apply(VariableSupplier vars) { return eval.apply(vars); } + } + + @RequiredArgsConstructor + public static final class BooleanToObjectFunction implements Function { + private final BooleanEval eval; + + @Override + public Object apply(VariableSupplier vars) { return eval.apply(vars); } + } + + @RequiredArgsConstructor + public static final class BooleanEvalPredicate implements ExpressionPredicate { + private final BooleanEval eval; + + @Override + public boolean test(VariableSupplier vars) { return eval.apply(vars); } + } + + @RequiredArgsConstructor + public static final class ObjectTernaryFunction implements Function { + private final BooleanEval condition; + private final Function ifTrue, ifFalse; + + @Override + public Object apply(VariableSupplier vars) { return condition.apply(vars) ? ifTrue.apply(vars) : ifFalse.apply(vars); } + } + + @RequiredArgsConstructor + public static final class IntConstant implements IntEval { + private final int value; + + @Override + public int apply(VariableSupplier vars) { return value; } + } + + @RequiredArgsConstructor + public static final class IntVariable implements IntEval { + private final String key; + + @Override + public int apply(VariableSupplier vars) { return vars.getInt(key); } + } + + @RequiredArgsConstructor + public static final class FloatConstant implements FloatEval { + private final float value; + + @Override + public float apply(VariableSupplier vars) { return value; } + } + + @RequiredArgsConstructor + public static final class FloatVariable implements FloatEval { + private final String key; + + @Override + public float apply(VariableSupplier vars) { return vars.getFloat(key); } + } + + @RequiredArgsConstructor + public static final class BooleanConstant implements BooleanEval { + private final boolean value; + + @Override + public boolean apply(VariableSupplier vars) { return value; } + } + + @RequiredArgsConstructor + public static final class BooleanVariable implements BooleanEval { + private final String key; + + @Override + public boolean apply(VariableSupplier vars) { return vars.getBoolean(key); } + } + + @RequiredArgsConstructor + public static final class IntTernary implements IntEval { + private final BooleanEval condition; + private final IntEval ifTrue, ifFalse; + + @Override + public int apply(VariableSupplier vars) { return condition.apply(vars) ? ifTrue.apply(vars) : ifFalse.apply(vars); } + } + + @RequiredArgsConstructor + public static final class FloatTernary implements FloatEval { + private final BooleanEval condition; + private final FloatEval ifTrue, ifFalse; + + @Override + public float apply(VariableSupplier vars) { return condition.apply(vars) ? ifTrue.apply(vars) : ifFalse.apply(vars); } + } + + @RequiredArgsConstructor + public static final class BooleanTernary implements BooleanEval { + private final BooleanEval condition, ifTrue, ifFalse; + + @Override + public boolean apply(VariableSupplier vars) { return condition.apply(vars) ? ifTrue.apply(vars) : ifFalse.apply(vars); } + } + + @RequiredArgsConstructor + public static final class IntMathOperation implements IntEval { + private final Operator op; + private final IntEval l, r; + + @Override + public int apply(VariableSupplier vars) { + final int lVal = l.apply(vars); + final int rVal = r.apply(vars); + + switch (op) { + case ADD: + return lVal + rVal; + case SUB: + return lVal - rVal; + case MUL: + return lVal * rVal; + case DIV: + return lVal / rVal; + case MOD: + return lVal % rVal; + } + + throw new UnsupportedOperationException("Operator '" + op + "' is not a math operator"); + } + } + + @RequiredArgsConstructor + public static final class FloatMathOperation implements FloatEval { + private final Operator op; + private final FloatEval l, r; + + @Override + public float apply(VariableSupplier vars) { + final float lVal = l.apply(vars); + final float rVal = r.apply(vars); + + switch (op) { + case ADD: + return lVal + rVal; + case SUB: + return lVal - rVal; + case MUL: + return lVal * rVal; + case DIV: + return lVal / rVal; + case MOD: + return lVal % rVal; + } + + throw new UnsupportedOperationException("Operator '" + op + "' is not a math operator"); + } + } + + @RequiredArgsConstructor + public static final class BooleanComparisons implements BooleanEval { + private final Operator op; + private final BooleanEval l, r; + + @Override + public boolean apply(VariableSupplier vars) { + // AND/OR both can short circuit based on lVal value, so rVal is being sampled as part of the check + final boolean lVal = l.apply(vars); + switch (op) { + case AND: + return lVal && r.apply(vars); + case OR: + return lVal || r.apply(vars); + case EQUAL: + return lVal == r.apply(vars); + case NOTEQUAL: + return lVal != r.apply(vars); + } + + throw new UnsupportedOperationException("Operator '" + op + "' is not a boolean comparison operator"); + } + } + + @RequiredArgsConstructor + public static final class BooleanNot implements BooleanEval { + private final BooleanEval operand; + + @Override + public boolean apply(VariableSupplier vars) { return !operand.apply(vars); } + } + + @RequiredArgsConstructor + public static final class IntComparisons implements BooleanEval { + private final Operator op; + private final IntEval l, r; + + @Override + public boolean apply(VariableSupplier vars) { + final int lVal = l.apply(vars); + final int rVal = r.apply(vars); + switch (op) { + case LESS: + return lVal < rVal; + case LEQUAL: + return lVal <= rVal; + case GREATER: + return lVal > rVal; + case GEQUAL: + return lVal >= rVal; + case EQUAL: + return lVal == rVal; + case NOTEQUAL: + return lVal != rVal; + } + + throw new UnsupportedOperationException("Operator '" + op + "' is not a int comparison operator"); + } + } + + @RequiredArgsConstructor + public static final class FloatComparisons implements BooleanEval { + private final Operator op; + private final FloatEval l, r; + + @Override + public boolean apply(VariableSupplier vars) { + final float lVal = l.apply(vars); + final float rVal = r.apply(vars); + switch (op) { + case LESS: + return lVal < rVal; + case LEQUAL: + return lVal <= rVal; + case GREATER: + return lVal > rVal; + case GEQUAL: + return lVal >= rVal; + case EQUAL: + return lVal == rVal; + case NOTEQUAL: + return lVal != rVal; + } + + throw new UnsupportedOperationException("Operator '" + op + "' is not a int comparison operator"); + } + } +} diff --git a/src/main/java/rs117/hd/utils/VariableSupplier.java b/src/main/java/rs117/hd/utils/VariableSupplier.java index 99f99b6258..c40e834ce5 100644 --- a/src/main/java/rs117/hd/utils/VariableSupplier.java +++ b/src/main/java/rs117/hd/utils/VariableSupplier.java @@ -6,18 +6,79 @@ public interface VariableSupplier { Object get(String name); + default float getFloat(String name) { + Object var = get(name); + if (var instanceof Integer) + return ((Integer) var).floatValue(); + return (int) var; + } + + default int getInt(String name) { + Object var = get(name); + if (var instanceof Float) + return ((Float) var).intValue(); + return (int) var; + } + + default boolean getBoolean(String name) { + return (Boolean) get(name); + } + default VariableSupplier proxy(VariableSupplier proxy) { - return name -> { + return new ProxyVariableSupplier(this, proxy); + } + + default VariableSupplier aliases(Map aliases) { + return proxy(aliases::get); + } + + final class ProxyVariableSupplier implements VariableSupplier { + private final VariableSupplier base; + private final VariableSupplier proxy; + + ProxyVariableSupplier(VariableSupplier base, VariableSupplier proxy) { + this.base = base; + this.proxy = proxy; + } + + @Override + public Object get(String name) { var value = proxy.get(name); if (value == null) - return get(name); + return base.get(name); if (value instanceof String) - return get((String) value); + return base.get((String) value); return value; - }; - } + } - default VariableSupplier aliases(Map aliases) { - return proxy(aliases::get); + @Override + public float getFloat(String name) { + var value = proxy.get(name); + if (value == null) + return base.getFloat(name); + if (value instanceof String) + return base.getFloat((String) value); + return (Float) value; + } + + @Override + public int getInt(String name) { + var value = proxy.get(name); + if (value == null) + return base.getInt(name); + if (value instanceof String) + return base.getInt((String) value); + return (Integer) value; + } + + @Override + public boolean getBoolean(String name) { + var value = proxy.get(name); + if (value == null) + return base.getBoolean(name); + if (value instanceof String) + return base.getBoolean((String) value); + return (Boolean) value; + } } } diff --git a/src/test/java/rs117/hd/tests/ExpressionParserTest.java b/src/test/java/rs117/hd/tests/ExpressionParserTest.java index 8756b58f1c..a165aa8865 100644 --- a/src/test/java/rs117/hd/tests/ExpressionParserTest.java +++ b/src/test/java/rs117/hd/tests/ExpressionParserTest.java @@ -13,40 +13,53 @@ public class ExpressionParserTest { @Test public void testExpressionParser() { - VariableSupplier vars = name -> { - switch (name) { - case "h": - return 5; - case "s": - return 10; - case "l": - return 5; - case "blending": - return true; - case "textures": - return false; + final VariableSupplier vars = new VariableSupplier() { + @Override + public Object get(String name) { return null; } + + @Override + public int getInt(String name) { + switch (name) { + case "h": + return 5; + case "s": + return 10; + case "l": + return 5; + } + throw new UnsupportedOperationException(name + " is not an int var"); + } + + @Override + public boolean getBoolean(String name) { + switch (name) { + case "blending": + return true; + case "textures": + return false; + } + throw new UnsupportedOperationException(name + " is not an boolean var"); } - return null; }; - Assert.assertEquals(5.f, parseExpression("5")); - Assert.assertEquals(-5.f, parseExpression("-5")); + Assert.assertEquals(5, parseExpression("5")); + Assert.assertEquals(-5, parseExpression("-5")); Assert.assertEquals(-2.5f, parseExpression("-2.5")); Assert.assertEquals(-.5f, parseExpression("-0.5")); Assert.assertEquals(-.5f, parseExpression("-.5")); Assert.assertEquals(.5f, parseExpression(".5")); Assert.assertEquals(.5f, parseExpression("+.5")); Assert.assertEquals(.5f, parseExpression("++ +.5")); - Assert.assertEquals(1f, parseExpression("--1")); + Assert.assertEquals(1, parseExpression("--1")); Assert.assertEquals(.5f, parseExpression("+-++-.5")); - Assert.assertEquals(17.f, parseFunction("5 + 12").apply(null)); - Assert.assertEquals(16.f, parseExpression("8 / 2 * (2 + 2)")); - Assert.assertEquals(32.f, parseExpression("2 * 8 / 2 * (2 + 2)")); - Assert.assertEquals(3.f, parseExpression("2 * 3 / 2")); - Assert.assertEquals(0.f, parseExpression("2 * 8 - 4 * 4")); - Assert.assertEquals(29.f, parseExpression("2 + 3 * (8 + 5 / 5)")); - Assert.assertEquals(40.f, parseExpression("(8 - 1 + 3) * 6 - ((3 + 7) * 2)")); - Assert.assertEquals(21.f, parseExpression("(1 + 2) * (3 + 4)")); + Assert.assertEquals(17, parseFunction("5 + 12").apply(null)); + Assert.assertEquals(16, parseExpression("8 / 2 * (2 + 2)")); + Assert.assertEquals(32, parseExpression("2 * 8 / 2 * (2 + 2)")); + Assert.assertEquals(3, parseExpression("2 * 3 / 2")); + Assert.assertEquals(0, parseExpression("2 * 8 - 4 * 4")); + Assert.assertEquals(29, parseExpression("2 + 3 * (8 + 5 / 5)")); + Assert.assertEquals(40, parseExpression("(8 - 1 + 3) * 6 - ((3 + 7) * 2)")); + Assert.assertEquals(21, parseExpression("(1 + 2) * (3 + 4)")); Assert.assertFalse(parsePredicate("!( blending )").test(vars)); Assert.assertEquals(false, parseExpression("!true")); Assert.assertEquals(true, parseExpression("SUMMER == 1", name -> SeasonalTheme.valueOf(name).ordinal()));