Skip to content

Commit

Permalink
Implement conditional constants (#1373)
Browse files Browse the repository at this point in the history
Signed-off-by: Pablo Herrera <[email protected]>
  • Loading branch information
Pablete1234 authored Aug 10, 2024
1 parent c6f6027 commit ee2b6b3
Show file tree
Hide file tree
Showing 4 changed files with 226 additions and 69 deletions.
146 changes: 146 additions & 0 deletions core/src/main/java/tc/oc/pgm/map/ConditionalChecker.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package tc.oc.pgm.map;

import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import org.jdom2.Element;
import tc.oc.pgm.util.platform.Platform;
import tc.oc.pgm.util.xml.InvalidXMLException;
import tc.oc.pgm.util.xml.Node;
import tc.oc.pgm.util.xml.XMLUtils;

class ConditionalChecker {

private static final List<AttributeCheck> ATTRIBUTES = List.of(
new AttributeCheck("variant", ConditionalChecker::variant),
new AttributeCheck("has-variant", ConditionalChecker::hasVariant),
new AttributeCheck("constant", ConditionalChecker::constant),
new AttributeCheck("min-server-version", ConditionalChecker::minServerVersion),
new AttributeCheck("max-server-version", ConditionalChecker::maxServerVersion));
private static final String ALL_ATTRS =
ATTRIBUTES.stream().map(AttributeCheck::key).collect(Collectors.joining("', '", "'", "'"));

/**
* Test if the current context passes the conditions declared in the element
*
* @param ctx the map's context
* @param el The conditional element
* @return if the conditional passes
* @throws InvalidXMLException if the element is invalid in any way
*/
static boolean test(MapFilePreprocessor ctx, Element el) throws InvalidXMLException {
Boolean result = null;
for (var check : ATTRIBUTES) {
Boolean attRes = check.apply(ctx, el);
if (attRes != null) result = result == null ? attRes : result && attRes;
}

if (result != null) return result;
throw new InvalidXMLException("Expected at least one of " + ALL_ATTRS + " attributes", el);
}

private static String[] split(String val) {
return val.split("[\\s,]+");
}

private static boolean variant(MapFilePreprocessor ctx, Element el, Node node) {
String value = node.getValue();
if (value.indexOf(',') == -1) return value.equals(ctx.getVariant());
return Set.of(split(value)).contains(ctx.getVariant());
}

private static boolean hasVariant(MapFilePreprocessor ctx, Element el, Node node) {
String value = node.getValue();
if (value.indexOf(',') == -1) return ctx.getVariantIds().contains(value);
return Arrays.stream(split(value)).anyMatch(ctx.getVariantIds()::contains);
}

private static boolean minServerVersion(MapFilePreprocessor ctx, Element el, Node node)
throws InvalidXMLException {
return Platform.MINECRAFT_VERSION.isNoOlderThan(XMLUtils.parseSemanticVersion(node));
}

private static boolean maxServerVersion(MapFilePreprocessor ctx, Element el, Node node)
throws InvalidXMLException {
return Platform.MINECRAFT_VERSION.isNoNewerThan(XMLUtils.parseSemanticVersion(node));
}

private static boolean constant(MapFilePreprocessor ctx, Element el, Node node)
throws InvalidXMLException {
var id = node.getValue();
var value = Node.fromAttr(el, "constant-value");
var cmp = XMLUtils.parseEnum(
Node.fromAttr(el, "constant-comparison"),
Cmp.class,
value == null ? Cmp.DEFINED : Cmp.EQUALS);

var constants = ctx.getConstants();
var isDefined = constants.containsKey(id);
var constant = isDefined ? constants.get(id) : null;

if (!cmp.requireValue && value != null)
throw new InvalidXMLException("Comparison type " + cmp + " should not have a value", value);

if (cmp.requireValue) {
if (value == null)
throw new InvalidXMLException("Required attribute 'constant-value' not set", el);

if (!isDefined)
throw new InvalidXMLException(
"Unknown constant '" + id + "'. Only constants before the conditional may be used.",
el);
if (constant == null) return false;
}

// The only reason these are split is for the IDE to infer nullability
if (!cmp.requireValue) {
return switch (cmp) {
case UNDEFINED -> !isDefined;
case DEFINED -> isDefined;
case DEFINED_DELETE -> isDefined && constant == null;
case DEFINED_VALUE -> isDefined && constant != null;
default -> throw new IllegalStateException("Unexpected value: " + cmp);
};
} else {
return switch (cmp) {
case EQUALS -> Objects.equals(value.getValue(), constant);
case CONTAINS -> Set.of(split(value.getValue())).contains(constant);
case REGEX -> constant.matches(value.getValue());
case RANGE -> XMLUtils.parseNumericRange(value, Double.class)
.contains(XMLUtils.parseNumber(new Node(el), constant, Double.class, true));
default -> throw new IllegalStateException("Unexpected value: " + cmp);
};
}
}

enum Cmp {
UNDEFINED(false),
DEFINED(false),
DEFINED_DELETE(false),
DEFINED_VALUE(false),
EQUALS(true),
CONTAINS(true),
REGEX(true),
RANGE(true);
private final boolean requireValue;

Cmp(boolean requireValue) {
this.requireValue = requireValue;
}
}

interface ElementPredicate {
boolean test(MapFilePreprocessor ctx, Element el, Node value) throws InvalidXMLException;
}

record AttributeCheck(String key, ElementPredicate pred) {
Boolean apply(MapFilePreprocessor ctx, Element el) throws InvalidXMLException {
var attr = Node.fromNullable(el.getAttribute(key));
if (attr == null) return null;

return pred.test(ctx, el, attr);
}
}
}
126 changes: 59 additions & 67 deletions core/src/main/java/tc/oc/pgm/map/MapFilePreprocessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
Expand All @@ -28,19 +26,16 @@
import tc.oc.pgm.api.map.includes.MapIncludeProcessor;
import tc.oc.pgm.util.xml.DocumentWrapper;
import tc.oc.pgm.util.xml.InvalidXMLException;
import tc.oc.pgm.util.xml.Node;
import tc.oc.pgm.util.xml.SAXHandler;
import tc.oc.pgm.util.xml.XMLUtils;

public class MapFilePreprocessor {

private static final ThreadLocal<SAXBuilder> DOCUMENT_FACTORY =
ThreadLocal.withInitial(
() -> {
final SAXBuilder builder = new SAXBuilder();
builder.setSAXHandlerFactory(SAXHandler.FACTORY);
return builder;
});
private static final ThreadLocal<SAXBuilder> DOCUMENT_FACTORY = ThreadLocal.withInitial(() -> {
final SAXBuilder builder = new SAXBuilder();
builder.setSAXHandlerFactory(SAXHandler.FACTORY);
return builder;
});

private static final Pattern CONSTANT_PATTERN = Pattern.compile("\\$\\{(.+?)}");

Expand Down Expand Up @@ -79,27 +74,16 @@ public Document getDocument()
variantIds.add(XMLUtils.parseRequiredId(variant));
}

document.runWithoutVisitation(
() -> {
MapInclude global = includeProcessor.getGlobalInclude();
if (global != null) {
document.getRootElement().addContent(0, global.getContent());
includes.add(global);
}

preprocessChildren(document.getRootElement());
source.setIncludes(includes);
});

for (Element constant :
XMLUtils.flattenElements(document.getRootElement(), "constants", "constant", 0)) {
boolean isDelete = XMLUtils.parseBoolean(constant.getAttribute("delete"), false);
String text = constant.getText();
if ((text == null || text.isEmpty()) != isDelete)
throw new InvalidXMLException(
"Delete attribute cannot be combined with having an inner text", constant);
constants.put(XMLUtils.parseRequiredId(constant), isDelete ? null : constant.getText());
}
document.runWithoutVisitation(() -> {
MapInclude global = includeProcessor.getGlobalInclude();
if (global != null) {
document.getRootElement().addContent(0, global.getContent());
includes.add(global);
}

preprocessChildren(document.getRootElement());
source.setIncludes(includes);
});

// If no constants are set, assume we can skip the step
if (!constants.isEmpty()) {
Expand All @@ -109,25 +93,31 @@ public Document getDocument()
return document;
}

String getVariant() {
return variant;
}

Set<String> getVariantIds() {
return variantIds;
}

Map<String, String> getConstants() {
return constants;
}

private void preprocessChildren(Element parent) throws InvalidXMLException {
for (int i = 0; i < parent.getContentSize(); i++) {
Content content = parent.getContent(i);
if (!(content instanceof Element)) continue;

Element child = (Element) content;
List<Content> replacement = null;

switch (child.getName()) {
case "include":
replacement = processIncludeElement(child);
break;
case "if":
replacement = processConditional(child, true);
break;
case "unless":
replacement = processConditional(child, false);
break;
}
if (!(content instanceof Element child)) continue;

List<Content> replacement =
switch (child.getName()) {
case "include" -> processIncludeElement(child);
case "if" -> processConditional(child, true);
case "unless" -> processConditional(child, false);
case "constant" -> processConstant(child);
default -> null;
};

if (replacement != null) {
parent.removeContent(i);
Expand All @@ -141,25 +131,29 @@ private void preprocessChildren(Element parent) throws InvalidXMLException {

private List<Content> processIncludeElement(Element element) throws InvalidXMLException {
MapInclude include = includeProcessor.getMapInclude(element);
if (include != null) {
includes.add(include);
return include.getContent();
}
return Collections.emptyList();
if (include == null) return List.of();
includes.add(include);
return include.getContent();
}

private List<Content> processConditional(Element el, boolean shouldContain)
throws InvalidXMLException {
private List<Content> processConditional(Element el, boolean expect) throws InvalidXMLException {
return ConditionalChecker.test(this, el) == expect ? el.cloneContent() : List.of();
}

private List<Content> processConstant(Element el) throws InvalidXMLException {
boolean isDelete = XMLUtils.parseBoolean(el.getAttribute("delete"), false);
String text = el.getTextNormalize();
if ((text == null || text.isEmpty()) != isDelete)
throw new InvalidXMLException(
"Delete attribute cannot be combined with having an inner text", el);

Node node = Node.fromRequiredAttr(el, "variant", "has-variant");
List<String> filter = Arrays.asList(node.getValue().split("[\\s,]+"));
var id = XMLUtils.parseRequiredId(el);
var value = isDelete ? null : text;

boolean contains =
"variant".equals(node.getName())
? filter.contains(this.variant)
: filter.stream().anyMatch(variantIds::contains);
boolean fallback = XMLUtils.parseBoolean(el.getAttribute("fallback"), false);
if (!fallback || !constants.containsKey(id)) constants.put(id, value);

return contains == shouldContain ? el.cloneContent() : Collections.emptyList();
return List.of();
}

private void postprocessChildren(Element parent) throws InvalidXMLException {
Expand All @@ -177,11 +171,9 @@ private void postprocessChildren(Element parent) throws InvalidXMLException {

for (int i = 0; i < parent.getContentSize(); i++) {
Content content = parent.getContent(i);
if (content instanceof Element) {
postprocessChildren((Element) content);
} else if (content instanceof Text) {
Text text = (Text) content;

if (content instanceof Element el) {
postprocessChildren(el);
} else if (content instanceof Text text) {
String result = postprocessString(parent, text.getText());
if (result == null) {
parent.removeContent(text);
Expand All @@ -196,7 +188,7 @@ private void postprocessChildren(Element parent) throws InvalidXMLException {
private @Nullable String postprocessString(Element el, String text) throws InvalidXMLException {
Matcher matcher = CONSTANT_PATTERN.matcher(text);

StringBuffer result = new StringBuffer();
StringBuilder result = new StringBuilder();
while (matcher.find()) {
String constant = matcher.group(1);
String replacement = constants.get(constant);
Expand Down
20 changes: 20 additions & 0 deletions util/src/main/java/tc/oc/pgm/util/Version.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ public boolean isNoOlderThan(Version other) {
return compareTo(other) >= 0;
}

/**
* Gets whether this version is less than or equal to another version.
*
* @param other Another version.
* @return If this version <= other version.
*/
public boolean isNoNewerThan(Version other) {
return compareTo(other) <= 0;
}

/**
* Gets whether this version is less than another version.
*
Expand All @@ -52,6 +62,16 @@ public boolean isOlderThan(Version other) {
return compareTo(other) < 0;
}

/**
* Gets whether this version is greater than another version.
*
* @param other Another version.
* @return If this version > other version.
*/
public boolean isNewerThan(Version other) {
return compareTo(other) > 0;
}

@Override
public int compareTo(Version other) {
int diff = major - other.major;
Expand Down
3 changes: 1 addition & 2 deletions util/src/main/java/tc/oc/pgm/util/xml/DocumentWrapper.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package tc.oc.pgm.util.xml;

import com.google.common.collect.Sets;
import java.util.Set;
import java.util.function.Consumer;
import org.jdom2.Attribute;
Expand All @@ -13,7 +12,7 @@
public class DocumentWrapper extends Document {

private static final Set<String> IGNORED =
Sets.newHashSet("name", "variant", "tutorial", "edition");
Set.of("constants", "edition", "name", "tutorial", "variant");

private boolean visitingAllowed = true;

Expand Down

0 comments on commit ee2b6b3

Please sign in to comment.