package com.xfestudio.xfeservermanager.core.trigger;
import com.xfestudio.xfeservermanager.api.trigger.TriggerExtension;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.HashMap;
import java.util.LinkedHashMap;
/** Type-driven six-part catalog consumed by the V2 editor and extension registry. */
public final class TriggerDescriptorCatalog {
public static final Set<String> FOUR_FORGE_TARGETS = Set.of(
"forge-1.20.1", "forge-1.20.6", "forge-1.21.1", "forge-1.21.11");
private static final Set<String> OWNER_ACTIONS = Set.of(
"server_command", "player_command", "set_time", "set_weather",
"spawn_entity", "damage_entity", "teleport_entity", "remove_entity",
"set_entity_attribute", "tag_entity", "set_block", "fill_blocks", "explosion",
"lightning", "set_difficulty", "set_gamerule", "world_border");
private static final Set<String> ADMIN_ACTIONS = Set.of(
"kick", "teleport", "give_item", "clear_inventory", "set_gamemode", "add_effect",
"remove_effects", "heal", "feed", "whitelist_add", "whitelist_remove", "ban", "pardon",
"economy_deposit", "economy_withdraw", "economy_set_balance", "economy_transfer",
"economy_deposit_player", "economy_withdraw_player", "economy_set_player_balance",
"economy_transfer_players", "set_health", "set_food", "set_experience",
"set_spawnpoint", "scoreboard_set", "scoreboard_add", "team_join", "team_leave",
"advancement_grant", "advancement_revoke");
public static final List<TriggerExtension.Descriptor> DESCRIPTORS = build();
public static final TriggerExtensionRegistry.Snapshot AVAILABLE =
TriggerExtensionRegistry.discover(DESCRIPTORS, TriggerDescriptorCatalog.class.getClassLoader());
private TriggerDescriptorCatalog() { }
public static TriggerExtension.Descriptor functionDescriptor(String id) {
if (id == null) return null;
String normalized = id.toLowerCase(java.util.Locale.ROOT);
return AVAILABLE.descriptors().stream().filter(descriptor -> descriptor.id().equals(normalized)
&& (descriptor.kind() == TriggerExtension.Kind.VALUE_FUNCTION
|| descriptor.kind() == TriggerExtension.Kind.CONDITION_FUNCTION))
.findFirst().orElse(null);
}
public static boolean hasFunctionHandler(String id) {
return id != null && AVAILABLE.functionHandlers().containsKey(id.toLowerCase(java.util.Locale.ROOT));
}
public static Object invokeFunction(String id, List<Object> arguments, Map<String, ?> event,
Map<String, Object> variables, long seed, java.time.Instant instant) {
String normalized = id.toLowerCase(java.util.Locale.ROOT);
TriggerExtension.FunctionHandler handler = AVAILABLE.functionHandlers().get(normalized);
if (handler == null) throw new IllegalArgumentException("unknown extension function: " + id);
Map<String, Object> safeEvent = transportMap(event);
Map<String, Object> safeVariables = transportMap(variables);
TriggerExtensionRegistry.validateTransportValue(arguments, 0);
try {
Object result = handler.evaluate(new TriggerExtension.FunctionInvocation(
normalized, arguments, safeEvent, safeVariables, seed, instant.toString()));
TriggerExtensionRegistry.validateTransportValue(result, 0);
return result;
} catch (RuntimeException failure) {
throw new IllegalStateException("trigger extension function failed: " + normalized, failure);
} catch (Exception failure) {
throw new IllegalStateException("trigger extension function failed: " + normalized, failure);
}
}
public static boolean hasActionHandler(String id) {
return id != null && AVAILABLE.actionHandlers().containsKey(id.toLowerCase(java.util.Locale.ROOT));
}
public static void invokeAction(String id, Map<String, Object> parameters, Map<String, ?> event,
String executionId, String nodeId, String idempotencyKey) {
String normalized = id.toLowerCase(java.util.Locale.ROOT);
TriggerExtension.ActionHandler handler = AVAILABLE.actionHandlers().get(normalized);
if (handler == null) throw new IllegalArgumentException("unknown extension action: " + id);
Map<String, Object> safeParameters = transportMap(parameters);
Map<String, Object> safeEvent = transportMap(event);
try {
handler.execute(new TriggerExtension.ActionInvocation(normalized, safeParameters, safeEvent,
executionId, nodeId, idempotencyKey));
} catch (RuntimeException failure) {
throw new IllegalStateException("trigger extension action failed: " + normalized, failure);
} catch (Exception failure) {
throw new IllegalStateException("trigger extension action failed: " + normalized, failure);
}
}
private static Map<String, Object> transportMap(Map<String, ?> values) {
Map<String, Object> copy = new LinkedHashMap<>();
if (values != null) values.forEach(copy::put);
TriggerExtensionRegistry.validateTransportValue(copy, 0);
return java.util.Collections.unmodifiableMap(copy);
}
/** Rejects unknown executable/event ids before a document can be persisted. */
public static void validateProgram(TriggerProgramV2 program) {
Map<String, TriggerExtension.Descriptor> events = new HashMap<>();
Map<String, TriggerExtension.Descriptor> actions = new HashMap<>();
for (TriggerExtension.Descriptor descriptor : AVAILABLE.descriptors()) {
if (descriptor.kind() == TriggerExtension.Kind.EVENT) events.put(descriptor.id(), descriptor);
if (descriptor.kind() == TriggerExtension.Kind.ACTION) actions.put(descriptor.id(), descriptor);
}
for (TriggerProgramV2.EventBinding event : program.events()) {
if (!events.containsKey(event.type()) && !event.type().startsWith("custom.")) {
throw new IllegalArgumentException("unknown trigger event: " + event.type());
}
}
program.statements().forEach(statement -> validateStatement(statement, actions));
program.functions().forEach(function ->
function.statements().forEach(statement -> validateStatement(statement, actions)));
}
private static void validateStatement(
TriggerProgramV2.Statement statement, Map<String, TriggerExtension.Descriptor> actions) {
if (statement.kind() == TriggerProgramV2.StatementKind.ACTION) {
TriggerExtension.Descriptor descriptor = actions.get(statement.name());
if (descriptor == null) throw new IllegalArgumentException("unknown trigger action: " + statement.name());
Set<String> parameterNames = descriptor.parameters().stream()
.map(TriggerExtension.Parameter::name).collect(java.util.stream.Collectors.toSet());
if (!parameterNames.containsAll(statement.inputs().keySet())) {
throw new IllegalArgumentException("action " + statement.name() + " has an unknown parameter");
}
if (!builtInAction(descriptor.id())) {
for (TriggerExtension.Parameter parameter : descriptor.parameters()) {
if (parameter.required() && !statement.inputs().containsKey(parameter.name())) {
throw new IllegalArgumentException("action " + statement.name()
+ " is missing parameter " + parameter.name());
}
}
if (!AVAILABLE.actionHandlers().containsKey(descriptor.id())) {
throw new IllegalArgumentException("extension action has no runtime handler: " + descriptor.id());
}
}
}
statement.statements().forEach(child -> validateStatement(child, actions));
statement.elseStatements().forEach(child -> validateStatement(child, actions));
statement.cases().forEach(branch ->
branch.statements().forEach(child -> validateStatement(child, actions)));
}
private static boolean builtInAction(String id) {
return DESCRIPTORS.stream().anyMatch(descriptor -> descriptor.kind() == TriggerExtension.Kind.ACTION
&& descriptor.id().equals(id));
}
private static List<TriggerExtension.Descriptor> build() {
List<TriggerExtension.Descriptor> result = new ArrayList<>();
for (String event : TriggerCatalog.EVENTS) {
result.add(descriptor(event, TriggerExtension.Kind.EVENT, event, "Trigger event", List.of(),
"event", TriggerExtension.Purity.SNAPSHOT_QUERY,
TriggerExtension.ThreadAffinity.SERVER, TriggerExtension.Risk.SAFE, Set.of("*")));
}
for (TriggerVariableCatalog.VariableDescriptor variable : TriggerVariableCatalog.VARIABLES) {
String responseId = "response." + variable.key().toLowerCase(java.util.Locale.ROOT)
.replaceAll("[^a-z0-9_.:_-]", "_");
result.add(descriptor(responseId, TriggerExtension.Kind.EVENT_RESPONSE,
variable.nameEn(), variable.descriptionEn(), List.of(), variable.type(),
TriggerExtension.Purity.PURE, TriggerExtension.ThreadAffinity.BACKGROUND,
TriggerExtension.Risk.SAFE, Set.copyOf(variable.events())));
}
Map<String, String> functions = Map.ofEntries(
Map.entry("len", "integer"), Map.entry("contains", "bool"),
Map.entry("starts_with", "bool"), Map.entry("ends_with", "bool"),
Map.entry("lower", "string"), Map.entry("upper", "string"),
Map.entry("abs", "float"), Map.entry("min", "float"), Map.entry("max", "float"),
Map.entry("clamp", "float"), Map.entry("random_float", "float"),
Map.entry("random_int", "integer"), Map.entry("instant", "instant"),
Map.entry("is_null", "bool"), Map.entry("chance", "bool"),
Map.entry("random_pick", "any"), Map.entry("distance", "float"),
Map.entry("has_tag", "bool"));
functions.forEach((name, type) -> result.add(descriptor(name,
TriggerExtension.Kind.VALUE_FUNCTION, name, "Pure snapshot function", List.of(), type,
TriggerExtension.Purity.PURE, TriggerExtension.ThreadAffinity.BACKGROUND,
TriggerExtension.Risk.SAFE, Set.of("*"))));
for (String operator : TriggerCatalog.OPERATORS) {
result.add(descriptor("compare." + operator, TriggerExtension.Kind.CONDITION_FUNCTION,
operator, "Typed comparison", List.of(parameter("left", "any"), parameter("right", "any")),
"bool", TriggerExtension.Purity.PURE, TriggerExtension.ThreadAffinity.BACKGROUND,
TriggerExtension.Risk.SAFE, Set.of("*")));
}
for (String action : TriggerCatalog.ACTIONS) {
List<TriggerExtension.Parameter> parameters = TriggerCatalog.ACTION_PARAMETERS.get(action).stream()
.map(name -> parameter(name, "expression<any>")).toList();
TriggerExtension.Risk risk = OWNER_ACTIONS.contains(action) ? TriggerExtension.Risk.OWNER
: ADMIN_ACTIONS.contains(action) ? TriggerExtension.Risk.ADMINISTRATOR
: TriggerExtension.Risk.SAFE;
result.add(descriptor(action, TriggerExtension.Kind.ACTION, action, "Trigger action", parameters,
"void", TriggerExtension.Purity.SIDE_EFFECT,
TriggerExtension.ThreadAffinity.SERVER, risk, Set.of("*")));
}
for (String type : TriggerValueTypes.VARIABLE_TYPES) {
result.add(descriptor(type, TriggerExtension.Kind.TYPE, type, "Trigger value type", List.of(),
type, TriggerExtension.Purity.PURE, TriggerExtension.ThreadAffinity.BACKGROUND,
TriggerExtension.Risk.SAFE, Set.of("*")));
}
return List.copyOf(result);
}
private static TriggerExtension.Parameter parameter(String name, String type) {
return new TriggerExtension.Parameter(name, type, true, null, "");
}
private static TriggerExtension.Descriptor descriptor(
String id,
TriggerExtension.Kind kind,
String name,
String description,
List<TriggerExtension.Parameter> parameters,
String returnType,
TriggerExtension.Purity purity,
TriggerExtension.ThreadAffinity affinity,
TriggerExtension.Risk risk,
Set<String> events) {
return new TriggerExtension.Descriptor(id, kind, name, description, parameters, returnType,
purity, affinity, risk, FOUR_FORGE_TARGETS, events, Map.of("builtIn", true));
}
}
package com.xfestudio.xfeservermanager.core.trigger;
import com.xfestudio.xfeservermanager.api.trigger.TriggerExtension;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.HashMap;
import java.util.LinkedHashMap;
/** Type-driven six-part catalog consumed by the V2 editor and extension registry. */
public final class TriggerDescriptorCatalog {
public static final Set<String> FOUR_FORGE_TARGETS = Set.of(
"forge-1.20.1", "forge-1.20.6", "forge-1.21.1", "forge-1.21.11");
private static final Set<String> OWNER_ACTIONS = Set.of(
"server_command", "player_command", "set_time", "set_weather",
"spawn_entity", "damage_entity", "teleport_entity", "remove_entity",
"set_entity_attribute", "tag_entity", "set_block", "fill_blocks", "explosion",
"lightning", "set_difficulty", "set_gamerule", "world_border");
private static final Set<String> ADMIN_ACTIONS = Set.of(
"kick", "teleport", "give_item", "clear_inventory", "set_gamemode", "add_effect",
"remove_effects", "heal", "feed", "whitelist_add", "whitelist_remove", "ban", "pardon",
"economy_deposit", "economy_withdraw", "economy_set_balance", "economy_transfer",
"economy_deposit_player", "economy_withdraw_player", "economy_set_player_balance",
"economy_transfer_players", "set_health", "set_food", "set_experience",
"set_spawnpoint", "scoreboard_set", "scoreboard_add", "team_join", "team_leave",
"advancement_grant", "advancement_revoke");
public static final List<TriggerExtension.Descriptor> DESCRIPTORS = build();
public static final TriggerExtensionRegistry.Snapshot AVAILABLE =
TriggerExtensionRegistry.discover(DESCRIPTORS, TriggerDescriptorCatalog.class.getClassLoader());
private TriggerDescriptorCatalog() { }
public static TriggerExtension.Descriptor functionDescriptor(String id) {
if (id == null) return null;
String normalized = id.toLowerCase(java.util.Locale.ROOT);
return AVAILABLE.descriptors().stream().filter(descriptor -> descriptor.id().equals(normalized)
&& (descriptor.kind() == TriggerExtension.Kind.VALUE_FUNCTION
|| descriptor.kind() == TriggerExtension.Kind.CONDITION_FUNCTION))
.findFirst().orElse(null);
}
public static boolean hasFunctionHandler(String id) {
return id != null && AVAILABLE.functionHandlers().containsKey(id.toLowerCase(java.util.Locale.ROOT));
}
public static Object invokeFunction(String id, List<Object> arguments, Map<String, ?> event,
Map<String, Object> variables, long seed, java.time.Instant instant) {
String normalized = id.toLowerCase(java.util.Locale.ROOT);
TriggerExtension.FunctionHandler handler = AVAILABLE.functionHandlers().get(normalized);
if (handler == null) throw new IllegalArgumentException("unknown extension function: " + id);
Map<String, Object> safeEvent = transportMap(event);
Map<String, Object> safeVariables = transportMap(variables);
TriggerExtensionRegistry.validateTransportValue(arguments, 0);
try {
Object result = handler.evaluate(new TriggerExtension.FunctionInvocation(
normalized, arguments, safeEvent, safeVariables, seed, instant.toString()));
TriggerExtensionRegistry.validateTransportValue(result, 0);
return result;
} catch (RuntimeException failure) {
throw new IllegalStateException("trigger extension function failed: " + normalized, failure);
} catch (Exception failure) {
throw new IllegalStateException("trigger extension function failed: " + normalized, failure);
}
}
public static boolean hasActionHandler(String id) {
return id != null && AVAILABLE.actionHandlers().containsKey(id.toLowerCase(java.util.Locale.ROOT));
}
public static void invokeAction(String id, Map<String, Object> parameters, Map<String, ?> event,
String executionId, String nodeId, String idempotencyKey) {
String normalized = id.toLowerCase(java.util.Locale.ROOT);
TriggerExtension.ActionHandler handler = AVAILABLE.actionHandlers().get(normalized);
if (handler == null) throw new IllegalArgumentException("unknown extension action: " + id);
Map<String, Object> safeParameters = transportMap(parameters);
Map<String, Object> safeEvent = transportMap(event);
try {
handler.execute(new TriggerExtension.ActionInvocation(normalized, safeParameters, safeEvent,
executionId, nodeId, idempotencyKey));
} catch (RuntimeException failure) {
throw new IllegalStateException("trigger extension action failed: " + normalized, failure);
} catch (Exception failure) {
throw new IllegalStateException("trigger extension action failed: " + normalized, failure);
}
}
private static Map<String, Object> transportMap(Map<String, ?> values) {
Map<String, Object> copy = new LinkedHashMap<>();
if (values != null) values.forEach(copy::put);
TriggerExtensionRegistry.validateTransportValue(copy, 0);
return java.util.Collections.unmodifiableMap(copy);
}
/** Rejects unknown executable/event ids before a document can be persisted. */
public static void validateProgram(TriggerProgramV2 program) {
Map<String, TriggerExtension.Descriptor> events = new HashMap<>();
Map<String, TriggerExtension.Descriptor> actions = new HashMap<>();
for (TriggerExtension.Descriptor descriptor : AVAILABLE.descriptors()) {
if (descriptor.kind() == TriggerExtension.Kind.EVENT) events.put(descriptor.id(), descriptor);
if (descriptor.kind() == TriggerExtension.Kind.ACTION) actions.put(descriptor.id(), descriptor);
}
for (TriggerProgramV2.EventBinding event : program.events()) {
if (!events.containsKey(event.type()) && !event.type().startsWith("custom.")) {
throw new IllegalArgumentException("unknown trigger event: " + event.type());
}
}
program.statements().forEach(statement -> validateStatement(statement, actions));
program.functions().forEach(function ->
function.statements().forEach(statement -> validateStatement(statement, actions)));
}
private static void validateStatement(
TriggerProgramV2.Statement statement, Map<String, TriggerExtension.Descriptor> actions) {
if (statement.kind() == TriggerProgramV2.StatementKind.ACTION) {
TriggerExtension.Descriptor descriptor = actions.get(statement.name());
if (descriptor == null) throw new IllegalArgumentException("unknown trigger action: " + statement.name());
Set<String> parameterNames = descriptor.parameters().stream()
.map(TriggerExtension.Parameter::name).collect(java.util.stream.Collectors.toSet());
if (!parameterNames.containsAll(statement.inputs().keySet())) {
throw new IllegalArgumentException("action " + statement.name() + " has an unknown parameter");
}
if (!builtInAction(descriptor.id())) {
for (TriggerExtension.Parameter parameter : descriptor.parameters()) {
if (parameter.required() && !statement.inputs().containsKey(parameter.name())) {
throw new IllegalArgumentException("action " + statement.name()
+ " is missing parameter " + parameter.name());
}
}
if (!AVAILABLE.actionHandlers().containsKey(descriptor.id())) {
throw new IllegalArgumentException("extension action has no runtime handler: " + descriptor.id());
}
}
}
statement.statements().forEach(child -> validateStatement(child, actions));
statement.elseStatements().forEach(child -> validateStatement(child, actions));
statement.cases().forEach(branch ->
branch.statements().forEach(child -> validateStatement(child, actions)));
}
private static boolean builtInAction(String id) {
return DESCRIPTORS.stream().anyMatch(descriptor -> descriptor.kind() == TriggerExtension.Kind.ACTION
&& descriptor.id().equals(id));
}
private static List<TriggerExtension.Descriptor> build() {
List<TriggerExtension.Descriptor> result = new ArrayList<>();
for (String event : TriggerCatalog.EVENTS) {
result.add(descriptor(event, TriggerExtension.Kind.EVENT, event, "Trigger event", List.of(),
"event", TriggerExtension.Purity.SNAPSHOT_QUERY,
TriggerExtension.ThreadAffinity.SERVER, TriggerExtension.Risk.SAFE, Set.of("*")));
}
for (TriggerVariableCatalog.VariableDescriptor variable : TriggerVariableCatalog.VARIABLES) {
String responseId = "response." + variable.key().toLowerCase(java.util.Locale.ROOT)
.replaceAll("[^a-z0-9_.:_-]", "_");
result.add(descriptor(responseId, TriggerExtension.Kind.EVENT_RESPONSE,
variable.nameEn(), variable.descriptionEn(), List.of(), variable.type(),
TriggerExtension.Purity.PURE, TriggerExtension.ThreadAffinity.BACKGROUND,
TriggerExtension.Risk.SAFE, Set.copyOf(variable.events())));
}
Map<String, String> functions = Map.ofEntries(
Map.entry("len", "integer"), Map.entry("contains", "bool"),
Map.entry("starts_with", "bool"), Map.entry("ends_with", "bool"),
Map.entry("lower", "string"), Map.entry("upper", "string"),
Map.entry("abs", "float"), Map.entry("min", "float"), Map.entry("max", "float"),
Map.entry("clamp", "float"), Map.entry("random_float", "float"),
Map.entry("random_int", "integer"), Map.entry("instant", "instant"),
Map.entry("is_null", "bool"), Map.entry("chance", "bool"),
Map.entry("random_pick", "any"), Map.entry("distance", "float"),
Map.entry("has_tag", "bool"));
functions.forEach((name, type) -> result.add(descriptor(name,
TriggerExtension.Kind.VALUE_FUNCTION, name, "Pure snapshot function", List.of(), type,
TriggerExtension.Purity.PURE, TriggerExtension.ThreadAffinity.BACKGROUND,
TriggerExtension.Risk.SAFE, Set.of("*"))));
for (String operator : TriggerCatalog.OPERATORS) {
result.add(descriptor("compare." + operator, TriggerExtension.Kind.CONDITION_FUNCTION,
operator, "Typed comparison", List.of(parameter("left", "any"), parameter("right", "any")),
"bool", TriggerExtension.Purity.PURE, TriggerExtension.ThreadAffinity.BACKGROUND,
TriggerExtension.Risk.SAFE, Set.of("*")));
}
for (String action : TriggerCatalog.ACTIONS) {
List<TriggerExtension.Parameter> parameters = TriggerCatalog.ACTION_PARAMETERS.get(action).stream()
.map(name -> parameter(name, "expression<any>")).toList();
TriggerExtension.Risk risk = OWNER_ACTIONS.contains(action) ? TriggerExtension.Risk.OWNER
: ADMIN_ACTIONS.contains(action) ? TriggerExtension.Risk.ADMINISTRATOR
: TriggerExtension.Risk.SAFE;
result.add(descriptor(action, TriggerExtension.Kind.ACTION, action, "Trigger action", parameters,
"void", TriggerExtension.Purity.SIDE_EFFECT,
TriggerExtension.ThreadAffinity.SERVER, risk, Set.of("*")));
}
for (String type : TriggerValueTypes.VARIABLE_TYPES) {
result.add(descriptor(type, TriggerExtension.Kind.TYPE, type, "Trigger value type", List.of(),
type, TriggerExtension.Purity.PURE, TriggerExtension.ThreadAffinity.BACKGROUND,
TriggerExtension.Risk.SAFE, Set.of("*")));
}
return List.copyOf(result);
}
private static TriggerExtension.Parameter parameter(String name, String type) {
return new TriggerExtension.Parameter(name, type, true, null, "");
}
private static TriggerExtension.Descriptor descriptor(
String id,
TriggerExtension.Kind kind,
String name,
String description,
List<TriggerExtension.Parameter> parameters,
String returnType,
TriggerExtension.Purity purity,
TriggerExtension.ThreadAffinity affinity,
TriggerExtension.Risk risk,
Set<String> events) {
return new TriggerExtension.Descriptor(id, kind, name, description, parameters, returnType,
purity, affinity, risk, FOUR_FORGE_TARGETS, events, Map.of("builtIn", true));
}
}