package com.xfestudio.xfeservermanager.core.policy;
import com.xfestudio.xfeservermanager.api.command.InvocationAst;
import com.xfestudio.xfeservermanager.api.command.ItemFact;
import com.xfestudio.xfeservermanager.api.command.SelectorFact;
import com.xfestudio.xfeservermanager.api.policy.CommandConstraints;
import com.xfestudio.xfeservermanager.api.policy.ItemConstraint;
import com.xfestudio.xfeservermanager.api.policy.PolicyViolation;
import com.xfestudio.xfeservermanager.api.policy.SelectorConstraint;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
public final class ConstraintEvaluator {
public List<PolicyViolation> evaluate(CommandConstraints constraints, InvocationAst invocation) {
List<PolicyViolation> violations = new ArrayList<>();
invocation.selectors().forEach(selector -> evaluateSelector(constraints.selector(), selector, violations));
invocation.items().forEach(item -> evaluateItem(constraints.item(), item, violations));
evaluateFunction(constraints, invocation, violations);
return List.copyOf(violations);
}
private static void evaluateSelector(
SelectorConstraint constraint, SelectorFact fact, List<PolicyViolation> violations) {
if (constraint.deniedSelectorTypes().contains(fact.selectorType())) {
boolean typedBroadSelector = fact.selectorType().isPotentiallyUnbounded()
&& constraint.allowBroadSelectorWithExplicitType()
&& hasSafelyBoundedExplicitType(fact);
if (!typedBroadSelector) {
add(violations, "selector_type_denied", fact.argumentName(),
"Selector type " + fact.selectorType() + " is denied");
}
}
if (constraint.deniedSorts().contains(fact.sort())) {
add(violations, "selector_sort_denied", fact.argumentName(),
"Selector sort " + fact.sort() + " is denied");
}
if (constraint.maximumTargets() != null) {
int upperBound = targetUpperBound(fact);
if (upperBound == SelectorFact.UNKNOWN_TARGET_COUNT || upperBound > constraint.maximumTargets()) {
add(violations, "selector_target_limit", fact.argumentName(),
"Selector may exceed the target limit of " + constraint.maximumTargets());
}
}
if (constraint.maximumDistance() != null) {
if (fact.distance() == null || fact.distance().maximum() == null
|| fact.distance().maximum() > constraint.maximumDistance()) {
add(violations, "selector_distance_limit", fact.argumentName(),
"Selector is not bounded to distance " + constraint.maximumDistance());
}
}
Set<String> effectiveTypes = fact.effectiveEntityTypes();
if (!constraint.allowedEntityTypes().isEmpty()) {
boolean emptyResolvedSelection = fact.resolutionSucceeded() && fact.resolvedTargetCount() == 0;
if (!emptyResolvedSelection
&& (effectiveTypes.isEmpty() || !constraint.allowedEntityTypes().containsAll(effectiveTypes))) {
add(violations, "entity_type_not_allowed", fact.argumentName(),
"Selector can resolve to an entity type outside the allow-list");
}
}
if (!constraint.deniedEntityTypes().isEmpty()) {
Set<String> deniedMatches = intersection(effectiveTypes, constraint.deniedEntityTypes());
if (!deniedMatches.isEmpty()) {
add(violations, "entity_type_denied", fact.argumentName(),
"Selector resolves to denied entity types " + deniedMatches);
} else if (deniedTypeMayStillResolve(constraint, fact, effectiveTypes)) {
add(violations, "entity_type_unresolved", fact.argumentName(),
"Selector entity types could not be resolved safely");
}
}
if (!constraint.allowTagFilters() && !fact.tags().isEmpty()) {
add(violations, "selector_tag_filter_denied", fact.argumentName(), "Selector tag filters are denied");
}
if (!constraint.allowNbtFilters() && fact.hasNbtFilter()) {
add(violations, "selector_nbt_filter_denied", fact.argumentName(), "Selector NBT filters are denied");
}
if (!constraint.allowPredicateFilters() && fact.hasPredicateFilter()) {
add(violations, "selector_predicate_filter_denied", fact.argumentName(),
"Selector predicate filters are denied");
}
}
private static boolean hasSafelyBoundedExplicitType(SelectorFact fact) {
// A type tag can expand after datapack reload and is not equivalent to one
// explicit registry id. Permit it only when the platform supplied the
// exhaustive set actually resolved for this invocation.
boolean hasConcreteIncludedType = fact.includedEntityTypes().stream()
.anyMatch(value -> !value.startsWith("#"));
if (hasConcreteIncludedType) {
return true;
}
boolean hasIncludedTypeTag = fact.includedEntityTypes().stream()
.anyMatch(value -> value.startsWith("#"));
// Resolved runtime types alone never prove that the command contained a type=
// option. In particular, bare @a resolves to minecraft:player and must remain
// denied when ALL_PLAYERS is on the denied-selector list.
return hasIncludedTypeTag
&& fact.resolutionSucceeded()
&& !fact.resolvedEntityTypes().isEmpty();
}
private static int targetUpperBound(SelectorFact fact) {
if (fact.resolvedTargetCount() != SelectorFact.UNKNOWN_TARGET_COUNT) {
return fact.resolvedTargetCount();
}
if (fact.limit() != null) {
return fact.limit();
}
return switch (fact.selectorType()) {
case SELF, NEAREST_PLAYER, RANDOM_PLAYER, DIRECT -> 1;
case ALL_PLAYERS, ALL_ENTITIES, UNKNOWN -> SelectorFact.UNKNOWN_TARGET_COUNT;
};
}
private static boolean mayResolveArbitraryType(SelectorFact fact) {
return fact.selectorType() == com.xfestudio.xfeservermanager.api.command.SelectorType.ALL_ENTITIES
|| fact.selectorType() == com.xfestudio.xfeservermanager.api.command.SelectorType.UNKNOWN;
}
private static boolean deniedTypeMayStillResolve(
SelectorConstraint constraint, SelectorFact fact, Set<String> effectiveTypes) {
if (fact.resolutionSucceeded()) {
// resolvedEntityTypes is exhaustive for this immediate invocation; an empty
// successful result means there is currently nothing for the command to affect.
return false;
}
if (!effectiveTypes.isEmpty()) {
return false;
}
if (!mayResolveArbitraryType(fact)) {
return false;
}
Set<String> remainingDenied = new HashSet<>(constraint.deniedEntityTypes());
fact.excludedEntityTypes().stream()
.filter(value -> !value.startsWith("#"))
.forEach(remainingDenied::remove);
return !remainingDenied.isEmpty();
}
private static void evaluateItem(ItemConstraint constraint, ItemFact fact, List<PolicyViolation> violations) {
if (!constraint.allowedItemIds().isEmpty() || !constraint.allowedItemTags().isEmpty()) {
boolean allowedById = constraint.allowedItemIds().contains(fact.itemId());
boolean allowedByTag = fact.itemTags().stream().anyMatch(constraint.allowedItemTags()::contains);
if (!allowedById && !allowedByTag) {
add(violations, "item_not_allowed", fact.argumentName(), "Item is not on the allow-list");
}
}
if (constraint.deniedItemIds().contains(fact.itemId())) {
add(violations, "item_denied", fact.argumentName(), "Item is explicitly denied");
}
Set<String> deniedTags = intersection(fact.itemTags(), constraint.deniedItemTags());
if (!deniedTags.isEmpty()) {
add(violations, "item_tag_denied", fact.argumentName(), "Item has denied tags " + deniedTags);
}
if (constraint.maximumCount() != null && fact.count() > constraint.maximumCount()) {
add(violations, "item_count_limit", fact.argumentName(),
"Item count exceeds " + constraint.maximumCount());
}
if (!constraint.allowedDataKeys().isEmpty()
&& !constraint.allowedDataKeys().containsAll(fact.dataKeys())) {
Set<String> unexpected = new HashSet<>(fact.dataKeys());
unexpected.removeAll(constraint.allowedDataKeys());
add(violations, "item_data_key_not_allowed", fact.argumentName(),
"Item contains data keys outside the allow-list " + unexpected);
}
Set<String> deniedKeys = intersection(fact.dataKeys(), constraint.deniedDataKeys());
if (!deniedKeys.isEmpty()) {
add(violations, "item_data_key_denied", fact.argumentName(),
"Item contains denied data keys " + deniedKeys);
}
}
private static void evaluateFunction(
CommandConstraints constraints, InvocationAst invocation, List<PolicyViolation> violations) {
if (!constraints.denyUnlistedFunctions() || !isFunctionInvocation(invocation)) {
return;
}
String functionId = invocation.scalarArguments().entrySet().stream()
.filter(entry -> entry.getKey().equalsIgnoreCase("function")
|| entry.getKey().equalsIgnoreCase("function_id")
// Vanilla's direct /function command names this Brigadier argument
// "name"; schedule function uses "function" on supported versions.
|| (invocation.commandId().equals("minecraft:function")
&& entry.getKey().equalsIgnoreCase("name")))
.map(java.util.Map.Entry::getValue)
.findFirst()
.map(ConstraintEvaluator::canonicalResourceId)
.orElse("");
if (functionId.isEmpty() || !constraints.allowedFunctionIds().contains(functionId)) {
add(violations, "function_not_allowed", "function",
functionId.isEmpty() ? "Function id is unknown" : "Function is not on the allow-list");
}
}
private static boolean isFunctionInvocation(InvocationAst invocation) {
if (invocation.commandId().equals("minecraft:function")) {
return true;
}
if (!invocation.commandId().equals("minecraft:schedule")) {
return false;
}
return invocation.commandPath().stream().anyMatch(part -> part.equals("function"));
}
private static String canonicalResourceId(String value) {
String normalized = value.trim().toLowerCase(Locale.ROOT);
return normalized.indexOf(':') < 0 ? "minecraft:" + normalized : normalized;
}
private static Set<String> intersection(Set<String> left, Set<String> right) {
Set<String> result = new HashSet<>(left);
result.retainAll(right);
return result;
}
private static void add(List<PolicyViolation> target, String code, String argumentName, String message) {
target.add(new PolicyViolation(code, argumentName, message));
}
}
package com.xfestudio.xfeservermanager.core.policy;
import com.xfestudio.xfeservermanager.api.command.InvocationAst;
import com.xfestudio.xfeservermanager.api.command.ItemFact;
import com.xfestudio.xfeservermanager.api.command.SelectorFact;
import com.xfestudio.xfeservermanager.api.policy.CommandConstraints;
import com.xfestudio.xfeservermanager.api.policy.ItemConstraint;
import com.xfestudio.xfeservermanager.api.policy.PolicyViolation;
import com.xfestudio.xfeservermanager.api.policy.SelectorConstraint;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
public final class ConstraintEvaluator {
public List<PolicyViolation> evaluate(CommandConstraints constraints, InvocationAst invocation) {
List<PolicyViolation> violations = new ArrayList<>();
invocation.selectors().forEach(selector -> evaluateSelector(constraints.selector(), selector, violations));
invocation.items().forEach(item -> evaluateItem(constraints.item(), item, violations));
evaluateFunction(constraints, invocation, violations);
return List.copyOf(violations);
}
private static void evaluateSelector(
SelectorConstraint constraint, SelectorFact fact, List<PolicyViolation> violations) {
if (constraint.deniedSelectorTypes().contains(fact.selectorType())) {
boolean typedBroadSelector = fact.selectorType().isPotentiallyUnbounded()
&& constraint.allowBroadSelectorWithExplicitType()
&& hasSafelyBoundedExplicitType(fact);
if (!typedBroadSelector) {
add(violations, "selector_type_denied", fact.argumentName(),
"Selector type " + fact.selectorType() + " is denied");
}
}
if (constraint.deniedSorts().contains(fact.sort())) {
add(violations, "selector_sort_denied", fact.argumentName(),
"Selector sort " + fact.sort() + " is denied");
}
if (constraint.maximumTargets() != null) {
int upperBound = targetUpperBound(fact);
if (upperBound == SelectorFact.UNKNOWN_TARGET_COUNT || upperBound > constraint.maximumTargets()) {
add(violations, "selector_target_limit", fact.argumentName(),
"Selector may exceed the target limit of " + constraint.maximumTargets());
}
}
if (constraint.maximumDistance() != null) {
if (fact.distance() == null || fact.distance().maximum() == null
|| fact.distance().maximum() > constraint.maximumDistance()) {
add(violations, "selector_distance_limit", fact.argumentName(),
"Selector is not bounded to distance " + constraint.maximumDistance());
}
}
Set<String> effectiveTypes = fact.effectiveEntityTypes();
if (!constraint.allowedEntityTypes().isEmpty()) {
boolean emptyResolvedSelection = fact.resolutionSucceeded() && fact.resolvedTargetCount() == 0;
if (!emptyResolvedSelection
&& (effectiveTypes.isEmpty() || !constraint.allowedEntityTypes().containsAll(effectiveTypes))) {
add(violations, "entity_type_not_allowed", fact.argumentName(),
"Selector can resolve to an entity type outside the allow-list");
}
}
if (!constraint.deniedEntityTypes().isEmpty()) {
Set<String> deniedMatches = intersection(effectiveTypes, constraint.deniedEntityTypes());
if (!deniedMatches.isEmpty()) {
add(violations, "entity_type_denied", fact.argumentName(),
"Selector resolves to denied entity types " + deniedMatches);
} else if (deniedTypeMayStillResolve(constraint, fact, effectiveTypes)) {
add(violations, "entity_type_unresolved", fact.argumentName(),
"Selector entity types could not be resolved safely");
}
}
if (!constraint.allowTagFilters() && !fact.tags().isEmpty()) {
add(violations, "selector_tag_filter_denied", fact.argumentName(), "Selector tag filters are denied");
}
if (!constraint.allowNbtFilters() && fact.hasNbtFilter()) {
add(violations, "selector_nbt_filter_denied", fact.argumentName(), "Selector NBT filters are denied");
}
if (!constraint.allowPredicateFilters() && fact.hasPredicateFilter()) {
add(violations, "selector_predicate_filter_denied", fact.argumentName(),
"Selector predicate filters are denied");
}
}
private static boolean hasSafelyBoundedExplicitType(SelectorFact fact) {
// A type tag can expand after datapack reload and is not equivalent to one
// explicit registry id. Permit it only when the platform supplied the
// exhaustive set actually resolved for this invocation.
boolean hasConcreteIncludedType = fact.includedEntityTypes().stream()
.anyMatch(value -> !value.startsWith("#"));
if (hasConcreteIncludedType) {
return true;
}
boolean hasIncludedTypeTag = fact.includedEntityTypes().stream()
.anyMatch(value -> value.startsWith("#"));
// Resolved runtime types alone never prove that the command contained a type=
// option. In particular, bare @a resolves to minecraft:player and must remain
// denied when ALL_PLAYERS is on the denied-selector list.
return hasIncludedTypeTag
&& fact.resolutionSucceeded()
&& !fact.resolvedEntityTypes().isEmpty();
}
private static int targetUpperBound(SelectorFact fact) {
if (fact.resolvedTargetCount() != SelectorFact.UNKNOWN_TARGET_COUNT) {
return fact.resolvedTargetCount();
}
if (fact.limit() != null) {
return fact.limit();
}
return switch (fact.selectorType()) {
case SELF, NEAREST_PLAYER, RANDOM_PLAYER, DIRECT -> 1;
case ALL_PLAYERS, ALL_ENTITIES, UNKNOWN -> SelectorFact.UNKNOWN_TARGET_COUNT;
};
}
private static boolean mayResolveArbitraryType(SelectorFact fact) {
return fact.selectorType() == com.xfestudio.xfeservermanager.api.command.SelectorType.ALL_ENTITIES
|| fact.selectorType() == com.xfestudio.xfeservermanager.api.command.SelectorType.UNKNOWN;
}
private static boolean deniedTypeMayStillResolve(
SelectorConstraint constraint, SelectorFact fact, Set<String> effectiveTypes) {
if (fact.resolutionSucceeded()) {
// resolvedEntityTypes is exhaustive for this immediate invocation; an empty
// successful result means there is currently nothing for the command to affect.
return false;
}
if (!effectiveTypes.isEmpty()) {
return false;
}
if (!mayResolveArbitraryType(fact)) {
return false;
}
Set<String> remainingDenied = new HashSet<>(constraint.deniedEntityTypes());
fact.excludedEntityTypes().stream()
.filter(value -> !value.startsWith("#"))
.forEach(remainingDenied::remove);
return !remainingDenied.isEmpty();
}
private static void evaluateItem(ItemConstraint constraint, ItemFact fact, List<PolicyViolation> violations) {
if (!constraint.allowedItemIds().isEmpty() || !constraint.allowedItemTags().isEmpty()) {
boolean allowedById = constraint.allowedItemIds().contains(fact.itemId());
boolean allowedByTag = fact.itemTags().stream().anyMatch(constraint.allowedItemTags()::contains);
if (!allowedById && !allowedByTag) {
add(violations, "item_not_allowed", fact.argumentName(), "Item is not on the allow-list");
}
}
if (constraint.deniedItemIds().contains(fact.itemId())) {
add(violations, "item_denied", fact.argumentName(), "Item is explicitly denied");
}
Set<String> deniedTags = intersection(fact.itemTags(), constraint.deniedItemTags());
if (!deniedTags.isEmpty()) {
add(violations, "item_tag_denied", fact.argumentName(), "Item has denied tags " + deniedTags);
}
if (constraint.maximumCount() != null && fact.count() > constraint.maximumCount()) {
add(violations, "item_count_limit", fact.argumentName(),
"Item count exceeds " + constraint.maximumCount());
}
if (!constraint.allowedDataKeys().isEmpty()
&& !constraint.allowedDataKeys().containsAll(fact.dataKeys())) {
Set<String> unexpected = new HashSet<>(fact.dataKeys());
unexpected.removeAll(constraint.allowedDataKeys());
add(violations, "item_data_key_not_allowed", fact.argumentName(),
"Item contains data keys outside the allow-list " + unexpected);
}
Set<String> deniedKeys = intersection(fact.dataKeys(), constraint.deniedDataKeys());
if (!deniedKeys.isEmpty()) {
add(violations, "item_data_key_denied", fact.argumentName(),
"Item contains denied data keys " + deniedKeys);
}
}
private static void evaluateFunction(
CommandConstraints constraints, InvocationAst invocation, List<PolicyViolation> violations) {
if (!constraints.denyUnlistedFunctions() || !isFunctionInvocation(invocation)) {
return;
}
String functionId = invocation.scalarArguments().entrySet().stream()
.filter(entry -> entry.getKey().equalsIgnoreCase("function")
|| entry.getKey().equalsIgnoreCase("function_id")
// Vanilla's direct /function command names this Brigadier argument
// "name"; schedule function uses "function" on supported versions.
|| (invocation.commandId().equals("minecraft:function")
&& entry.getKey().equalsIgnoreCase("name")))
.map(java.util.Map.Entry::getValue)
.findFirst()
.map(ConstraintEvaluator::canonicalResourceId)
.orElse("");
if (functionId.isEmpty() || !constraints.allowedFunctionIds().contains(functionId)) {
add(violations, "function_not_allowed", "function",
functionId.isEmpty() ? "Function id is unknown" : "Function is not on the allow-list");
}
}
private static boolean isFunctionInvocation(InvocationAst invocation) {
if (invocation.commandId().equals("minecraft:function")) {
return true;
}
if (!invocation.commandId().equals("minecraft:schedule")) {
return false;
}
return invocation.commandPath().stream().anyMatch(part -> part.equals("function"));
}
private static String canonicalResourceId(String value) {
String normalized = value.trim().toLowerCase(Locale.ROOT);
return normalized.indexOf(':') < 0 ? "minecraft:" + normalized : normalized;
}
private static Set<String> intersection(Set<String> left, Set<String> right) {
Set<String> result = new HashSet<>(left);
result.retainAll(right);
return result;
}
private static void add(List<PolicyViolation> target, String code, String argumentName, String message) {
target.add(new PolicyViolation(code, argumentName, message));
}
}