XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEServerManager

【Java】我的世界XFE服务器管理器

公开
关注 0 Fork 0 Star 0
UTF-8
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));
    }
}