Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement conditional constants #1373

Merged
merged 2 commits into from
Aug 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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