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

XFEServerManager

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

公开
关注 0 Fork 0 Star 0
UTF-8
package com.xfestudio.xfeservermanager.core.trigger;

import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;

/** Durable, platform-neutral trigger document used by both the visual and script editors. */
public record TriggerDefinition(
        UUID id,
        UUID groupId,
        String name,
        String description,
        boolean enabled,
        Mode mode,
        TriggerEvent event,
        MatchMode conditionMode,
        List<Condition> conditions,
        List<Action> actions,
        String script,
        long revision,
        String createdBy,
        Instant createdAt,
        Instant updatedAt,
        boolean migrated) {

    public static final int MAX_SCRIPT_CHARACTERS = 65_536;

    public TriggerDefinition {
        Objects.requireNonNull(id, "id");
        Objects.requireNonNull(groupId, "groupId");
        name = bounded(name, "name", 1, 120);
        description = bounded(description == null ? "" : description, "description", 0, 500);
        Objects.requireNonNull(mode, "mode");
        Objects.requireNonNull(event, "event");
        Objects.requireNonNull(conditionMode, "conditionMode");
        conditions = checkedList(conditions, "condition");
        actions = checkedList(actions, "action");
        script = script == null ? "" : script;
        createdBy = bounded(createdBy, "createdBy", 1, 120);
        Objects.requireNonNull(createdAt, "createdAt");
        Objects.requireNonNull(updatedAt, "updatedAt");
        TriggerActionTree.validate(actions);
        if (script.length() > MAX_SCRIPT_CHARACTERS) {
            throw new IllegalArgumentException("script cannot exceed 65536 characters");
        }
        if (revision < 0) throw new IllegalArgumentException("revision must not be negative");
        if (mode == Mode.CODE) {
            Program compiled = TriggerScriptCompiler.compile(script);
            event = compiled.event();
            conditionMode = compiled.conditionMode();
            conditions = compiled.conditions();
            actions = compiled.actions();
        } else {
            TriggerActionTree.validateProgram(new Program(event, conditionMode, conditions, actions));
        }
    }

    public Program program() {
        return new Program(event, conditionMode, conditions, actions);
    }

    public Program executableProgram() {
        // CODE documents are canonicalized once by the constructor, so high-frequency events do
        // not repeatedly parse a potentially large script. The script remains the editable source.
        return program();
    }

    private static String bounded(String value, String field, int minimum, int maximum) {
        if (value == null) throw new IllegalArgumentException(field + " is required");
        String normalized = value.strip();
        if (normalized.length() < minimum || normalized.length() > maximum) {
            throw new IllegalArgumentException(field + " length must be between " + minimum + " and " + maximum);
        }
        return normalized;
    }

    public enum Mode { VISUAL, CODE }

    public enum MatchMode { ALL, ANY }

    public record TriggerEvent(String type, Map<String, String> configuration,
                               List<CommandArgument> arguments,
                               List<VariableDefinition> variables) {
        /** Source and persistence compatibility for every pre-parameter-tree trigger. */
        public TriggerEvent(String type, Map<String, String> configuration) {
            this(type, configuration, List.of(), List.of());
        }

        public TriggerEvent {
            type = bounded(type, "event.type", 1, 80).toLowerCase(java.util.Locale.ROOT);
            configuration = new java.util.LinkedHashMap<>(configuration == null ? Map.of() : configuration);
            if (configuration.size() > 16) {
                throw new IllegalArgumentException("event configuration has too many parameters");
            }
            configuration.forEach((key, value) -> {
                bounded(key, "event configuration parameter", 1, 80);
                if (value == null || value.length() > 4_096) {
                    throw new IllegalArgumentException("event configuration value is too long");
                }
            });
            // The script compiler creates an empty event placeholder when it reads `on` and
            // supplies the collected `set` values in the final Program. Leave that placeholder
            // alone so validation can report a missing interval after the whole script is read.
            if (type.equals("schedule.interval") && !configuration.isEmpty()) {
                configuration = new java.util.LinkedHashMap<>(
                        TriggerIntervalSchedule.normalizeConfiguration(configuration));
            }
            configuration = Map.copyOf(configuration);
            arguments = checkedList(arguments, "command argument");
            variables = checkedList(variables, "trigger variable");
            java.util.LinkedHashSet<String> variableNames = new java.util.LinkedHashSet<>();
            for (VariableDefinition variable : variables) {
                if (!variableNames.add(variable.contextKey())) {
                    throw new IllegalArgumentException("duplicate trigger variable: " + variable.contextKey());
                }
            }
        }
    }

    /** A recursive Brigadier branch. Siblings are alternatives and children are the next level. */
    public record CommandArgument(
            String name, String type, String literal, boolean optional, String errorMessage,
            String minimum, String maximum, List<String> suggestions, List<CommandArgument> children) {
        public CommandArgument {
            name = bounded(name, "command argument name", 1, 32).toLowerCase(java.util.Locale.ROOT);
            if (!name.matches("[a-z][a-z0-9_-]{0,31}")) {
                throw new IllegalArgumentException("command argument name contains unsupported characters");
            }
            type = TriggerValueTypes.normalizeCommandType(type);
            literal = literal == null ? "" : literal.strip();
            errorMessage = bounded(errorMessage == null ? "" : errorMessage,
                    "command argument error", 0, 256);
            minimum = minimum == null ? "" : minimum.strip();
            maximum = maximum == null ? "" : maximum.strip();
            suggestions = checkedList(suggestions, "command argument suggestion").stream()
                    .map(value -> bounded(value, "command argument suggestion", 1, 128)).distinct().toList();
            if (suggestions.size() > 64) throw new IllegalArgumentException("too many command argument suggestions");
            children = checkedList(children, "command argument child");
            TriggerValueTypes.validateCommandLiteral(type, literal, minimum, maximum);
        }
    }

    /** Strongly typed scoped state exposed as {@code var.<name>} or {@code global.<name>}. */
    public record VariableDefinition(
            String name, String type, String initialValue, String visibility, String storage,
            String lifetime, Long ttlSeconds, long revision) {
        /** Migrates every v1 declaration to trigger-local, trigger-owned storage. */
        public VariableDefinition(String name, String type, String initialValue) {
            this(name, type, initialValue, "trigger", "trigger", "session", null, 0L);
        }

        /** Source and JSON compatibility for declarations created before lifecycle support. */
        public VariableDefinition(
                String name, String type, String initialValue, String visibility, String storage) {
            this(name, type, initialValue, visibility, storage, "session", null, 0L);
        }

        public VariableDefinition {
            name = bounded(name, "trigger variable name", 1, 48);
            if (!name.matches("[A-Za-z_][A-Za-z0-9_-]{0,47}")) {
                throw new IllegalArgumentException("trigger variable name contains unsupported characters");
            }
            type = TriggerValueTypes.normalizeVariableType(type);
            initialValue = initialValue == null ? "" : initialValue;
            visibility = visibility == null || visibility.isBlank()
                    ? "trigger" : visibility.strip().toLowerCase(java.util.Locale.ROOT);
            storage = storage == null || storage.isBlank()
                    ? (visibility.equals("global") ? "server" : "trigger")
                    : storage.strip().toLowerCase(java.util.Locale.ROOT);
            lifetime = lifetime == null || lifetime.isBlank()
                    ? "session" : lifetime.strip().toLowerCase(java.util.Locale.ROOT);
            if (!java.util.Set.of("trigger", "global").contains(visibility)) {
                throw new IllegalArgumentException("variable visibility must be trigger or global");
            }
            if (!java.util.Set.of(
                    "server", "player", "dimension", "trigger", "execution", "chunk", "entity")
                    .contains(storage)) {
                throw new IllegalArgumentException("unsupported variable storage scope: " + storage);
            }
            if (!java.util.Set.of("session", "ttl", "persistent").contains(lifetime)) {
                throw new IllegalArgumentException("unsupported variable lifetime: " + lifetime);
            }
            if (lifetime.equals("ttl")) {
                if (ttlSeconds == null || ttlSeconds < 1 || ttlSeconds > 31_536_000L) {
                    throw new IllegalArgumentException(
                            "ttl variable requires ttlSeconds between 1 and 31536000");
                }
            } else {
                ttlSeconds = null;
            }
            if (revision < 0) throw new IllegalArgumentException("variable revision must not be negative");
            TriggerValueTypes.parseVariable(type, initialValue);
        }

        public String contextKey() {
            return (visibility.equals("global") ? "global." : "var.") + name;
        }
    }

    public record Condition(String field, String operator, String value) {
        public Condition {
            field = bounded(field, "condition.field", 1, 120);
            operator = bounded(operator, "condition.operator", 1, 30)
                    .toLowerCase(java.util.Locale.ROOT);
            value = value == null ? "" : value;
            if (value.length() > 4_096) throw new IllegalArgumentException("condition value is too long");
        }
    }

    public record Action(String type, Map<String, String> parameters, List<Action> children) {
        /** Source-compatible constructor for existing callers and persisted two-field actions. */
        public Action(String type, Map<String, String> parameters) {
            this(type, parameters, List.of());
        }

        public Action {
            type = bounded(type, "action.type", 1, 80).toLowerCase(java.util.Locale.ROOT);
            parameters = new java.util.LinkedHashMap<>(parameters == null ? Map.of() : parameters);
            children = checkedList(children, "action child");
            if (parameters.size() > 16) throw new IllegalArgumentException("action has too many parameters");
            parameters.forEach((key, value) -> {
                bounded(key, "action parameter", 1, 80);
                if (value == null) throw new IllegalArgumentException("action parameter value is required");
                if (value.length() > 65_536) {
                    throw new IllegalArgumentException("action parameter is too long");
                }
            });
            if (type.equals(TriggerActionTree.CONDITION_TYPE)) {
                for (String parameter : parameters.keySet()) {
                    if (!java.util.Set.of("field", "operator", "value").contains(parameter)) {
                        throw new IllegalArgumentException(
                                "condition does not accept parameter " + parameter);
                    }
                }
                Condition condition = new Condition(
                        parameters.get("field"), parameters.get("operator"),
                        parameters.getOrDefault("value", ""));
                java.util.LinkedHashMap<String, String> normalized = new java.util.LinkedHashMap<>();
                normalized.put("field", condition.field());
                normalized.put("operator", condition.operator());
                normalized.put("value", condition.value());
                parameters = normalized;
            } else if (!children.isEmpty()) {
                throw new IllegalArgumentException("only condition actions may contain child actions");
            }
            parameters = Map.copyOf(parameters);
        }
    }

    public record Program(TriggerEvent event, MatchMode conditionMode,
                          List<Condition> conditions, List<Action> actions) {
        public Program {
            Objects.requireNonNull(event, "event");
            Objects.requireNonNull(conditionMode, "conditionMode");
            conditions = checkedList(conditions, "condition");
            actions = checkedList(actions, "action");
            TriggerActionTree.validate(actions);
        }
    }

    private static <T> List<T> checkedList(List<T> values, String elementName) {
        if (values == null) return List.of();
        if (values.stream().anyMatch(Objects::isNull)) {
            throw new IllegalArgumentException(elementName + " must not be null");
        }
        return List.copyOf(values);
    }
}