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);
}
}
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);
}
}