diff --git a/RemotelyMod/libs/ReScreen-1.0.jar b/RemotelyMod/libs/ReScreen-1.0.jar index c2e73339..7bd36f94 100644 Binary files a/RemotelyMod/libs/ReScreen-1.0.jar and b/RemotelyMod/libs/ReScreen-1.0.jar differ diff --git a/RemotelyMod/libs/Rebase-1.0-SNAPSHOT.jar b/RemotelyMod/libs/Rebase-1.0-SNAPSHOT.jar index c03264ac..e22064c2 100644 Binary files a/RemotelyMod/libs/Rebase-1.0-SNAPSHOT.jar and b/RemotelyMod/libs/Rebase-1.0-SNAPSHOT.jar differ diff --git a/RemotelyMod/libs/Remotely-App.jar b/RemotelyMod/libs/Remotely-App.jar index 385ae9c5..472478fd 100644 Binary files a/RemotelyMod/libs/Remotely-App.jar and b/RemotelyMod/libs/Remotely-App.jar differ diff --git a/libs/ReScreen-1.0.jar b/libs/ReScreen-1.0.jar index c2e73339..7bd36f94 100644 Binary files a/libs/ReScreen-1.0.jar and b/libs/ReScreen-1.0.jar differ diff --git a/libs/Rebase-1.0-SNAPSHOT.jar b/libs/Rebase-1.0-SNAPSHOT.jar index c03264ac..e22064c2 100644 Binary files a/libs/Rebase-1.0-SNAPSHOT.jar and b/libs/Rebase-1.0-SNAPSHOT.jar differ diff --git a/src/main/java/redxax/oxy/remotely/data/flow/ReSyncFlowClient.java b/src/main/java/redxax/oxy/remotely/data/flow/ReSyncFlowClient.java index 7378aec7..887f29a2 100644 --- a/src/main/java/redxax/oxy/remotely/data/flow/ReSyncFlowClient.java +++ b/src/main/java/redxax/oxy/remotely/data/flow/ReSyncFlowClient.java @@ -4,12 +4,16 @@ import org.java_websocket.handshake.ServerHandshake; import redxax.oxy.remotely.RemotelyClient; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import redxax.oxy.remotely.flow.data.FlowDataType; +import redxax.oxy.remotely.flow.data.FlowDataTypeAdapter; import redxax.oxy.remotely.flow.data.FlowGraph; import redxax.oxy.remotely.flow.data.GuiDefinition; import redxax.oxy.remotely.flow.data.ScoreboardDefinition; import redxax.oxy.remotely.flow.data.TabDefinition; import redxax.oxy.remotely.flow.data.TriggerBinding; import redxax.oxy.remotely.flow.cache.NodeRegistryCache; +import redxax.oxy.remotely.flow.registry.NodeDefinition; import redxax.oxy.remotely.flow.registry.NodeRegistry; import redxax.oxy.remotely.flow.sync.NodeRegistryRequest; import redxax.oxy.remotely.flow.sync.NodeRegistrySnapshot; @@ -24,6 +28,7 @@ import restudio.rescreen.util.Notification; import restudio.rebase.restudio.api.ReStudioApiClient; +import java.io.IOException; import java.net.URI; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -68,7 +73,21 @@ public interface ErrorListener { private static final short CONTROL_CHANNEL_ID = 0; private int sequenceCounter = 0; private ErrorListener errorListener; - private final Gson gson = new Gson(); + private final Gson gson = new GsonBuilder() + .registerTypeAdapter(FlowDataType.class, new FlowDataTypeAdapter()) + .registerTypeAdapter(NodeDefinition.NodeCategory.class, new com.google.gson.TypeAdapter() { + @Override + public void write(com.google.gson.stream.JsonWriter out, NodeDefinition.NodeCategory value) throws IOException { + out.value(value != null ? value.getId() : null); + } + + @Override + public NodeDefinition.NodeCategory read(com.google.gson.stream.JsonReader in) throws IOException { + String id = in.nextString(); + return NodeDefinition.NodeCategory.fromString(id); + } + }) + .create(); private final Queue pendingSends = new ConcurrentLinkedQueue<>(); private final Map> pendingOpenResources = new ConcurrentHashMap<>(); private final NodeRegistryCache nodeRegistryCache = NodeRegistryCache.getInstance(); diff --git a/src/main/java/redxax/oxy/remotely/flow/data/FlowDataType.java b/src/main/java/redxax/oxy/remotely/flow/data/FlowDataType.java new file mode 100644 index 00000000..1736cab4 --- /dev/null +++ b/src/main/java/redxax/oxy/remotely/flow/data/FlowDataType.java @@ -0,0 +1,148 @@ +package redxax.oxy.remotely.flow.data; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class FlowDataType { + private static final Map REGISTRY = new LinkedHashMap<>(); + + public static final FlowDataType EXECUTION = new FlowDataType("execution", null, 0xFFFFFF, "Execution"); + public static final FlowDataType ANY = new FlowDataType("any", null, 0x808080, "Any"); + public static final FlowDataType STRING = new FlowDataType("string", null, 0xDA00FF, "String"); + public static final FlowDataType NUMBER = new FlowDataType("number", null, 0x00FF93, "Number"); + public static final FlowDataType BOOLEAN = new FlowDataType("boolean", null, 0xD20000, "Boolean"); + public static final FlowDataType ENTITY = new FlowDataType("entity", null, 0x8B4513, "Entity"); + public static final FlowDataType LIVING_ENTITY = new FlowDataType("living_entity", ENTITY, 0xA0522D, "Living Entity"); + public static final FlowDataType PLAYER = new FlowDataType("player", LIVING_ENTITY, 0x0066FF, "Player"); + public static final FlowDataType MATERIAL = new FlowDataType("material", null, 0x00AA00, "Material"); + public static final FlowDataType BLOCK = new FlowDataType("block", null, 0x228B22, "Block"); + public static final FlowDataType ITEM = new FlowDataType("item", MATERIAL, 0x32CD32, "Item"); + public static final FlowDataType ITEMSTACK = ITEM; + public static final FlowDataType WORLD = new FlowDataType("world", null, 0x00CED1, "World"); + public static final FlowDataType BIOME = new FlowDataType("biome", null, 0x20B2AA, "Biome"); + public static final FlowDataType LOCATION = new FlowDataType("location", null, 0xFFA500, "Location"); + public static final FlowDataType VECTOR = new FlowDataType("vector", null, 0x7FFFD4, "Vector"); + public static final FlowDataType COLOR = new FlowDataType("color", null, 0xFF66CC, "Color"); + public static final FlowDataType UUID = new FlowDataType("uuid", null, 0x708090, "UUID"); + public static final FlowDataType GAMEMODE = new FlowDataType("gamemode", null, 0x4169E1, "Gamemode"); + public static final FlowDataType DIFFICULTY = new FlowDataType("difficulty", null, 0xDC143C, "Difficulty"); + public static final FlowDataType ENTITY_TYPE = new FlowDataType("entity_type", null, 0xCD853F, "Entity Type"); + public static final FlowDataType ENCHANTMENT = new FlowDataType("enchantment", null, 0x9370DB, "Enchantment"); + public static final FlowDataType INVENTORY = new FlowDataType("inventory", null, 0x4682B4, "Inventory"); + public static final FlowDataType POTION_EFFECT = new FlowDataType("potion_effect", null, 0xFF1493, "Potion Effect"); + public static final FlowDataType SOUND = new FlowDataType("sound", null, 0xFFB347, "Sound"); + public static final FlowDataType ADVANCEMENT = new FlowDataType("advancement", null, 0xFFD700, "Advancement"); + public static final FlowDataType PERMISSION_GROUP = new FlowDataType("permission_group", null, 0x6A5ACD, "Permission Group"); + public static final FlowDataType SCOREBOARD = new FlowDataType("scoreboard", null, 0x1E90FF, "Scoreboard"); + public static final FlowDataType TEAM = new FlowDataType("team", null, 0x00BFFF, "Team"); + public static final FlowDataType REGION = new FlowDataType("region", null, 0x9ACD32, "Region"); + public static final FlowDataType COMPONENT = new FlowDataType("component", STRING, 0xE066FF, "Component"); + public static final FlowDataType JSON_OBJECT = new FlowDataType("json_object", null, 0x4B0082, "JSON Object"); + public static final FlowDataType LIST = new FlowDataType("list", null, 0xFF69B4, "List"); + public static final FlowDataType MAP = new FlowDataType("map", null, 0x9932CC, "Map"); + public static final FlowDataType SET = new FlowDataType("set", null, 0xFF4500, "Set"); + public static final FlowDataType QUEUE = new FlowDataType("queue", null, 0x2E8B57, "Queue"); + public static final FlowDataType STACK = new FlowDataType("stack", null, 0x4682B4, "Stack"); + + private final String id; + private final FlowDataType parent; + private final int color; + private final String displayName; + private final boolean canStringify; + + private FlowDataType(String id, FlowDataType parent, int color, String displayName) { + this(id, parent, color, displayName, !"execution".equals(id) && !"any".equals(id)); + } + + private FlowDataType(String id, FlowDataType parent, int color, String displayName, boolean canStringify) { + this.id = id; + this.parent = parent; + this.color = color; + this.displayName = displayName; + this.canStringify = canStringify; + REGISTRY.put(id, this); + } + + static { + REGISTRY.put("flow", EXECUTION); + } + + public String getId() { + return id; + } + + public FlowDataType getParent() { + return parent; + } + + public int getColor() { + return 0xFF000000 | color; + } + + public String getDisplayName() { + return displayName; + } + + public boolean isAssignableFrom(FlowDataType other) { + if (this == ANY || this == other) return true; + if (other == null) return false; + if (other.parent != null && (this == other.parent || isAssignableFrom(other.parent))) return true; + return false; + } + + public boolean canConvertTo(FlowDataType target) { + if (target == null) return false; + if (target.isAssignableFrom(this)) return true; + if (target == STRING) return canStringify(); + return false; + } + + public boolean canStringify() { + return canStringify; + } + + public static List values() { + return REGISTRY.values().stream().distinct().filter(type -> type != EXECUTION).toList(); + } + + public static FlowDataType fromString(String id) { + if (id == null || id.isEmpty()) return ANY; + FlowDataType type = REGISTRY.get(id.toLowerCase()); + return type != null ? type : ANY; + } + + public static FlowDataType registerServerType(String id, String displayName, int color, String parentId, boolean canStringify) { + if (id == null || id.isBlank()) { + return ANY; + } + FlowDataType parent = parentId != null ? fromString(parentId) : null; + if (parent == ANY && parentId != null && !parentId.equalsIgnoreCase("any")) { + parent = null; + } + FlowDataType existing = REGISTRY.get(id.toLowerCase()); + if (existing != null) { + return existing; + } + FlowDataType type = new FlowDataType(id, parent, color, displayName, canStringify); + return type; + } + + @Override + public String toString() { + return id; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof FlowDataType)) return false; + FlowDataType that = (FlowDataType) o; + return id.equals(that.id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } +} diff --git a/src/main/java/redxax/oxy/remotely/flow/data/FlowDataTypeAdapter.java b/src/main/java/redxax/oxy/remotely/flow/data/FlowDataTypeAdapter.java new file mode 100644 index 00000000..80451dbe --- /dev/null +++ b/src/main/java/redxax/oxy/remotely/flow/data/FlowDataTypeAdapter.java @@ -0,0 +1,24 @@ +package redxax.oxy.remotely.flow.data; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; + +public class FlowDataTypeAdapter extends TypeAdapter { + @Override + public void write(JsonWriter out, FlowDataType value) throws IOException { + if (value == null) { + out.nullValue(); + } else { + out.value(value.getId()); + } + } + + @Override + public FlowDataType read(JsonReader in) throws IOException { + String id = in.nextString(); + return FlowDataType.fromString(id); + } +} diff --git a/src/main/java/redxax/oxy/remotely/flow/data/FlowGraph.java b/src/main/java/redxax/oxy/remotely/flow/data/FlowGraph.java index 4523a205..543d62f6 100644 --- a/src/main/java/redxax/oxy/remotely/flow/data/FlowGraph.java +++ b/src/main/java/redxax/oxy/remotely/flow/data/FlowGraph.java @@ -17,16 +17,16 @@ public class FlowGraph { public static class FunctionParameter { private String name; - private FlowType type; + private FlowDataType type; public FunctionParameter() { this.name = ""; - this.type = FlowType.ANY; + this.type = FlowDataType.ANY; } - public FunctionParameter(String name, FlowType type) { + public FunctionParameter(String name, FlowDataType type) { this.name = name; - this.type = type != null ? type : FlowType.ANY; + this.type = type != null ? type : FlowDataType.ANY; } public String getName() { @@ -37,11 +37,11 @@ public void setName(String name) { this.name = name; } - public FlowType getType() { + public FlowDataType getType() { return type; } - public void setType(FlowType type) { + public void setType(FlowDataType type) { this.type = type; } } diff --git a/src/main/java/redxax/oxy/remotely/flow/data/FlowSerializer.java b/src/main/java/redxax/oxy/remotely/flow/data/FlowSerializer.java index 5c16e639..32078683 100644 --- a/src/main/java/redxax/oxy/remotely/flow/data/FlowSerializer.java +++ b/src/main/java/redxax/oxy/remotely/flow/data/FlowSerializer.java @@ -6,6 +6,7 @@ public class FlowSerializer { private static final Gson gson = new GsonBuilder() .setPrettyPrinting() + .registerTypeAdapter(FlowDataType.class, new FlowDataTypeAdapter()) .create(); public static String serialize(FlowGraph graph) { diff --git a/src/main/java/redxax/oxy/remotely/flow/data/FlowType.java b/src/main/java/redxax/oxy/remotely/flow/data/FlowType.java deleted file mode 100644 index 11bf76ea..00000000 --- a/src/main/java/redxax/oxy/remotely/flow/data/FlowType.java +++ /dev/null @@ -1,66 +0,0 @@ -package redxax.oxy.remotely.flow.data; - -public enum FlowType { - EXECUTION(0xFFFFFF, "Execution"), - STRING(0xDA00FF, "String"), - NUMBER(0x00FF93, "Number"), - BOOLEAN(0xD20000, "Boolean"), - PLAYER(0x0066FF, "Player"), - LOCATION(0xFFA500, "Location"), - ITEM(0x00AA00, "Item"), - LIST(0xFF69B4, "List"), - ENTITY(0x8B4513, "Entity"), - ITEMSTACK(0x32CD32, "ItemStack"), - JSON_OBJECT(0x4B0082, "JSONObject"), - ANY(0x808080, "Any"); - - private final int color; - private final String displayName; - - FlowType(int color, String displayName) { - this.color = color & 0xFFFFFF; - this.displayName = displayName; - } - - public int getColor() { - return 0xFF000000 | color; - } - - public String getDisplayName() { - return displayName; - } - - public boolean isCompatibleWith(FlowType other) { - if (this == ANY || other == ANY) { - return true; - } - if ((this == PLAYER && other == ENTITY) || (this == ENTITY && other == PLAYER)) { - return true; - } - return this == other; - } - - public static FlowType fromString(String name) { - for (FlowType type : values()) { - if (type.name().equalsIgnoreCase(name)) { - return type; - } - } - return ANY; - } - - public static FlowType fromClassName(String className) { - String lower = className.toLowerCase(); - if (lower.contains("list") || lower.contains("collection") || lower.contains("array")) return LIST; - if (lower.contains("entity")) return ENTITY; - if (lower.contains("itemstack")) return ITEMSTACK; - if (lower.contains("json") || lower.contains("map")) return JSON_OBJECT; - if (lower.contains("string")) return STRING; - if (lower.contains("int") || lower.contains("double") || lower.contains("float") || lower.contains("long")) return NUMBER; - if (lower.contains("bool")) return BOOLEAN; - if (lower.contains("player")) return PLAYER; - if (lower.contains("location") || lower.contains("vector")) return LOCATION; - if (lower.contains("item")) return ITEM; - return ANY; - } -} diff --git a/src/main/java/redxax/oxy/remotely/flow/registry/NodeDefinition.java b/src/main/java/redxax/oxy/remotely/flow/registry/NodeDefinition.java index 72c05417..ea610848 100644 --- a/src/main/java/redxax/oxy/remotely/flow/registry/NodeDefinition.java +++ b/src/main/java/redxax/oxy/remotely/flow/registry/NodeDefinition.java @@ -1,8 +1,11 @@ package redxax.oxy.remotely.flow.registry; -import redxax.oxy.remotely.flow.data.FlowType; +import redxax.oxy.remotely.flow.data.FlowDataType; + import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -18,24 +21,116 @@ public enum PinDirection { OUTPUT } - public enum NodeCategory { + public static final class NodeCategory { + private static final Map REGISTRY = new LinkedHashMap<>(); + + public static final NodeCategory EVENT = new NodeCategory("event", "Event", 0xFFFF5555, 100); + public static final NodeCategory ACTION = new NodeCategory("action", "Action", 0xFF5555FF, 200); + public static final NodeCategory LOGIC = new NodeCategory("logic", "Logic", 0xFFFF55FF, 300); + public static final NodeCategory DATA = new NodeCategory("data", "Data", 0xFF55FFFF, 400); + public static final NodeCategory VARIABLE = new NodeCategory("variable", "Variable", 0xFFFFFF55, 500); + public static final NodeCategory FUNCTION = new NodeCategory("function", "Function", 0xFFFFAA55, 600); + public static final NodeCategory ENTITY = new NodeCategory("entity", "Entity", 0xFF8B4513, 700); + public static final NodeCategory BLOCK = new NodeCategory("block", "Block", 0xFF228B22, 800); + public static final NodeCategory WORLD = new NodeCategory("world", "World", 0xFF228B22, 900); + public static final NodeCategory INVENTORY = new NodeCategory("inventory", "Inventory", 0xFF00CED1, 1000); + public static final NodeCategory ITEM = new NodeCategory("item", "Item", 0xFF32CD32, 1100); + public static final NodeCategory SCOREBOARD = new NodeCategory("scoreboard", "Scoreboard", 0xFFDAA520, 1200); + public static final NodeCategory ECONOMY = new NodeCategory("economy", "Economy", 0xFFFFFF00, 1300); + public static final NodeCategory PERMISSION = new NodeCategory("permission", "Permission", 0xFFBA55D3, 1400); + public static final NodeCategory VISUAL = new NodeCategory("visual", "Visual", 0xFFFF1493, 1500); + public static final NodeCategory DATABASE = new NodeCategory("database", "Database", 0xFF4B0082, 1600); + public static final NodeCategory HTTP = new NodeCategory("http", "HTTP", 0xFFFF6347, 1700); + public static final NodeCategory DISCORD = new NodeCategory("discord", "Discord", 0xFF7289DA, 1800); + public static final NodeCategory UTILITY = new NodeCategory("utility", "Utility", 0xFFA9A9A9, 1900); + + private final String id; + private final String displayName; + private final int color; + private final int priority; + + private NodeCategory(String id, String displayName, int color, int priority) { + this.id = id; + this.displayName = displayName; + this.color = color; + this.priority = priority; + REGISTRY.put(id, this); + } + + public String getId() { + return id; + } + + public String getDisplayName() { + return displayName; + } + + public int getColor() { + return color; + } + + public int getPriority() { + return priority; + } + + public static NodeCategory fromString(String id) { + if (id == null || id.isBlank()) { + return UTILITY; + } + NodeCategory cat = REGISTRY.get(id.toLowerCase()); + return cat != null ? cat : UTILITY; + } + + public static List values() { + return List.copyOf(REGISTRY.values()); + } + + public static NodeCategory registerServerCategory(String id, String displayName, int color, int priority) { + NodeCategory existing = REGISTRY.get(id.toLowerCase()); + if (existing != null) { + return existing; + } + return new NodeCategory(id, displayName, color, priority); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof NodeCategory)) return false; + NodeCategory that = (NodeCategory) o; + return id.equals(that.id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public String toString() { + return id; + } + } + + public enum WidgetType { + AUTO, + TEXT, + TOGGLE, + DROPDOWN, + SEARCHABLE_LIST, + SLIDER, + NUMBER, + MULTILINE, + COLOR + } + + public enum NodeKind { EVENT, ACTION, - LOGIC, - DATA, - VARIABLE, - FUNCTION, - ENTITY, - WORLD, - INVENTORY, - DATABASE, - HTTP, - DISCORD, - ECONOMY, - PERMISSION, - VISUAL, - SCOREBOARD, - UTILITY + QUERY, + PURE, + FAMILY, + ALIAS } private final String id; @@ -46,6 +141,19 @@ public enum NodeCategory { private final int color; private final int priority; private final boolean hidden; + private final String description; + private final String handler; + private final Map handlerConfig; + private final boolean trigger; + private final String eventType; + private final List aliases; + private final List outputMappings; + private final int schemaVersion; + private final NodeKind kind; + private final Availability availability; + private final String canonicalId; + private final List legacyIds; + private final boolean deprecated; private NodeDefinition(Builder builder) { this.id = builder.id; @@ -56,6 +164,19 @@ private NodeDefinition(Builder builder) { this.color = builder.color; this.priority = builder.priority; this.hidden = builder.hidden; + this.description = builder.description; + this.handler = builder.handler; + this.handlerConfig = builder.handlerConfig; + this.trigger = builder.trigger; + this.eventType = builder.eventType; + this.aliases = builder.aliases; + this.outputMappings = builder.outputMappings; + this.schemaVersion = builder.schemaVersion; + this.kind = builder.kind; + this.availability = builder.availability; + this.canonicalId = builder.canonicalId; + this.legacyIds = builder.legacyIds; + this.deprecated = builder.deprecated; } public String getId() { @@ -90,17 +211,146 @@ public boolean isHidden() { return hidden; } + public String getDescription() { + return description; + } + + public String getHandler() { + return handler; + } + + public Map getHandlerConfig() { + return handlerConfig; + } + + public boolean isTrigger() { + return trigger; + } + + public String getEventType() { + return eventType; + } + + public List getAliases() { + return aliases; + } + + public List getOutputMappings() { + return outputMappings; + } + + public int getSchemaVersion() { + return schemaVersion; + } + + public NodeKind getKind() { + return kind; + } + + public Availability getAvailability() { + return availability; + } + + public String getCanonicalId() { + return canonicalId; + } + + public List getLegacyIds() { + return legacyIds; + } + + public boolean isDeprecated() { + return deprecated; + } + + public record PinMapping(String source, String target) { + } + + public static class Availability { + private final String plugin; + private final String platform; + private final String minVersion; + + public Availability(String plugin, String platform, String minVersion) { + this.plugin = plugin; + this.platform = platform; + this.minVersion = minVersion; + } + + public String getPlugin() { + return plugin; + } + + public String getPlatform() { + return platform; + } + + public String getMinVersion() { + return minVersion; + } + } + + public static class PinConstraints { + private final Double min; + private final Double max; + private final Double step; + + public PinConstraints(Double min, Double max, Double step) { + this.min = min; + this.max = max; + this.step = step; + } + + public Double getMin() { + return min; + } + + public Double getMax() { + return max; + } + + public Double getStep() { + return step; + } + } + public static class PinDefinition { private final String name; private final PinType type; private final PinDirection direction; - private final FlowType dataType; + private final FlowDataType dataType; + private final WidgetType widgetType; + private final List options; + private final String optionsSource; + private final String defaultValue; + private final PinConstraints constraints; + private final Map visibleWhen; + private final String description; - public PinDefinition(String name, PinType type, PinDirection direction, FlowType dataType) { + public PinDefinition(String name, PinType type, PinDirection direction, FlowDataType dataType) { + this(name, type, direction, dataType, null, null, null, null, null, null, null); + } + + public PinDefinition(String name, PinType type, PinDirection direction, FlowDataType dataType, + WidgetType widgetType, List options, String defaultValue, + PinConstraints constraints, Map visibleWhen, String description) { + this(name, type, direction, dataType, widgetType, options, null, defaultValue, constraints, visibleWhen, description); + } + + public PinDefinition(String name, PinType type, PinDirection direction, FlowDataType dataType, + WidgetType widgetType, List options, String optionsSource, String defaultValue, + PinConstraints constraints, Map visibleWhen, String description) { this.name = name; this.type = type; this.direction = direction; this.dataType = dataType; + this.widgetType = widgetType; + this.options = options != null ? options : Collections.emptyList(); + this.optionsSource = optionsSource; + this.defaultValue = defaultValue; + this.constraints = constraints; + this.visibleWhen = visibleWhen != null ? visibleWhen : Collections.emptyMap(); + this.description = description; } public String getName() { @@ -115,9 +365,37 @@ public PinDirection getDirection() { return direction; } - public FlowType getDataType() { + public FlowDataType getDataType() { return dataType; } + + public WidgetType getWidgetType() { + return widgetType; + } + + public List getOptions() { + return options; + } + + public String getOptionsSource() { + return optionsSource; + } + + public String getDefaultValue() { + return defaultValue; + } + + public PinConstraints getConstraints() { + return constraints; + } + + public Map getVisibleWhen() { + return visibleWhen; + } + + public String getDescription() { + return description; + } } public static class Builder { @@ -129,6 +407,19 @@ public static class Builder { private int color = 0xFFAAAAAA; private int priority = 0; private boolean hidden = false; + private String description; + private String handler; + private Map handlerConfig; + private boolean trigger = false; + private String eventType; + private List aliases = Collections.emptyList(); + private List outputMappings = Collections.emptyList(); + private int schemaVersion = 1; + private NodeKind kind; + private Availability availability; + private String canonicalId; + private List legacyIds = Collections.emptyList(); + private boolean deprecated; public Builder(String id, String displayName, NodeCategory category) { this.id = id; @@ -136,41 +427,34 @@ public Builder(String id, String displayName, NodeCategory category) { this.category = category; } - public Builder input(String name, PinType type, FlowType dataType) { + public Builder input(String name, PinType type, FlowDataType dataType) { inputs.add(new PinDefinition(name, type, PinDirection.INPUT, dataType)); return this; } - public Builder output(String name, PinType type, FlowType dataType) { + public Builder input(PinDefinition pin) { + inputs.add(pin); + return this; + } + + public Builder output(String name, PinType type, FlowDataType dataType) { outputs.add(new PinDefinition(name, type, PinDirection.OUTPUT, dataType)); return this; } + public Builder output(PinDefinition pin) { + outputs.add(pin); + return this; + } + public Builder color(int color) { this.color = color; return this; } public Builder color(NodeCategory category) { - switch (category) { - case EVENT: this.color = 0xFFFF5555; break; - case ACTION: this.color = 0xFF5555FF; break; - case LOGIC: this.color = 0xFFFF55FF; break; - case DATA: this.color = 0xFF55FFFF; break; - case VARIABLE: this.color = 0xFFFFFF55; break; - case FUNCTION: this.color = 0xFFFFAA55; break; - case ENTITY: this.color = 0xFF8B4513; break; - case WORLD: this.color = 0xFF228B22; break; - case INVENTORY: this.color = 0xFF00CED1; break; - case DATABASE: this.color = 0xFF4B0082; break; - case HTTP: this.color = 0xFFFF6347; break; - case DISCORD: this.color = 0xFF7289DA; break; - case ECONOMY: this.color = 0xFFFFFF00; break; - case PERMISSION: this.color = 0xFFBA55D3; break; - case VISUAL: this.color = 0xFFFF1493; break; - case SCOREBOARD: this.color = 0xFFDAA520; break; - case UTILITY: this.color = 0xFFA9A9A9; break; - default: this.color = 0xFFAAAAAA; + if (category != null) { + this.color = category.getColor(); } return this; } @@ -185,6 +469,71 @@ public Builder hidden(boolean hidden) { return this; } + public Builder description(String description) { + this.description = description; + return this; + } + + public Builder handler(String handler) { + this.handler = handler; + return this; + } + + public Builder handlerConfig(Map handlerConfig) { + this.handlerConfig = handlerConfig; + return this; + } + + public Builder trigger(boolean trigger) { + this.trigger = trigger; + return this; + } + + public Builder eventType(String eventType) { + this.eventType = eventType; + return this; + } + + public Builder aliases(List aliases) { + this.aliases = aliases != null ? aliases : Collections.emptyList(); + return this; + } + + public Builder outputMappings(List outputMappings) { + this.outputMappings = outputMappings != null ? outputMappings : Collections.emptyList(); + return this; + } + + public Builder schemaVersion(int schemaVersion) { + this.schemaVersion = schemaVersion; + return this; + } + + public Builder kind(NodeKind kind) { + this.kind = kind; + return this; + } + + public Builder availability(Availability availability) { + this.availability = availability; + return this; + } + + public Builder canonicalId(String canonicalId) { + this.canonicalId = canonicalId; + return this; + } + + public Builder legacyIds(List legacyIds) { + this.legacyIds = legacyIds != null ? legacyIds : Collections.emptyList(); + return this; + } + + public Builder deprecated(boolean deprecated) { + this.deprecated = deprecated; + return this; + } + public Builder hidden() { return hidden(true); } @@ -193,7 +542,99 @@ public NodeDefinition build() { if (color == 0xFFAAAAAA && category != null) { color(category); } + if (kind == null) { + kind = inferKind(); + } return new NodeDefinition(this); } + + private NodeKind inferKind() { + if (trigger) { + return NodeKind.EVENT; + } + if (canonicalId != null && !canonicalId.isBlank() && hidden) { + return NodeKind.ALIAS; + } + if (handler != null && List.of("player", "entity", "world", "block", "inventory", "itemstack").contains(handler)) { + return NodeKind.FAMILY; + } + boolean hasFlowInput = inputs.stream().anyMatch(pin -> pin.getType() == PinType.FLOW && pin.getDirection() == PinDirection.INPUT); + boolean hasFlowOutput = outputs.stream().anyMatch(pin -> pin.getType() == PinType.FLOW && pin.getDirection() == PinDirection.OUTPUT); + if (hasFlowInput || hasFlowOutput) { + return NodeKind.ACTION; + } + return NodeKind.QUERY; + } + } + + public static class PinBuilder { + private String name; + private PinType type; + private PinDirection direction; + private FlowDataType dataType; + private WidgetType widgetType; + private List options; + private String optionsSource; + private String defaultValue; + private PinConstraints constraints; + private Map visibleWhen; + private String description; + + public PinBuilder(String name, PinType type, PinDirection direction, FlowDataType dataType) { + this.name = name; + this.type = type; + this.direction = direction; + this.dataType = dataType; + } + + public PinBuilder widget(WidgetType widgetType) { + this.widgetType = widgetType; + return this; + } + + public PinBuilder options(List options) { + this.options = options; + return this; + } + + public PinBuilder optionsSource(String optionsSource) { + this.optionsSource = optionsSource; + return this; + } + + public PinBuilder defaultValue(String defaultValue) { + this.defaultValue = defaultValue; + return this; + } + + public PinBuilder constraints(Double min, Double max, Double step) { + this.constraints = new PinConstraints(min, max, step); + return this; + } + + public PinBuilder visibleWhen(String pinName, String expectedValue) { + if (this.visibleWhen == null) { + this.visibleWhen = new HashMap<>(); + } + this.visibleWhen.put(pinName, expectedValue); + return this; + } + + public PinBuilder visibleWhen(Map conditions) { + if (this.visibleWhen == null) { + this.visibleWhen = new HashMap<>(); + } + this.visibleWhen.putAll(conditions); + return this; + } + + public PinBuilder description(String description) { + this.description = description; + return this; + } + + public PinDefinition build() { + return new PinDefinition(name, type, direction, dataType, widgetType, options, optionsSource, defaultValue, constraints, visibleWhen, description); + } } } diff --git a/src/main/java/redxax/oxy/remotely/flow/registry/NodeRegistry.java b/src/main/java/redxax/oxy/remotely/flow/registry/NodeRegistry.java index 52631ff4..f613e3a9 100644 --- a/src/main/java/redxax/oxy/remotely/flow/registry/NodeRegistry.java +++ b/src/main/java/redxax/oxy/remotely/flow/registry/NodeRegistry.java @@ -1,5 +1,6 @@ package redxax.oxy.remotely.flow.registry; +import redxax.oxy.remotely.flow.data.FlowDataType; import redxax.oxy.remotely.flow.sync.NodePluginPayload; import redxax.oxy.remotely.flow.sync.NodeRegistrySnapshot; import java.util.ArrayList; @@ -17,6 +18,12 @@ public class NodeRegistry { private final Map> serverDefinitions = new ConcurrentHashMap<>(); private final Map> serverPlugins = new ConcurrentHashMap<>(); private final Map> serverNodeIds = new ConcurrentHashMap<>(); + private final Map>>> serverPropertyActions = new ConcurrentHashMap<>(); + private final Map>> serverPropertyOutputTypes = new ConcurrentHashMap<>(); + private final Map> serverTypeMetadata = new ConcurrentHashMap<>(); + private final Map> serverCategoryMetadata = new ConcurrentHashMap<>(); + private final Map> serverOptionSourceMetadata = new ConcurrentHashMap<>(); + private final Map> serverConversionRules = new ConcurrentHashMap<>(); private final List listeners = new CopyOnWriteArrayList<>(); private static NodeRegistry INSTANCE; @@ -119,6 +126,32 @@ public void applySnapshot(String serverId, NodeRegistrySnapshot snapshot) { serverNodeIds.put(key, new ArrayList<>(snapshot.getNodeIds())); } + if (snapshot.getPropertyActions() != null) { + serverPropertyActions.put(key, snapshot.getPropertyActions()); + } + if (snapshot.getPropertyOutputTypes() != null) { + serverPropertyOutputTypes.put(key, snapshot.getPropertyOutputTypes()); + } + + if (snapshot.getTypeMetadata() != null) { + serverTypeMetadata.put(key, new ArrayList<>(snapshot.getTypeMetadata())); + for (redxax.oxy.remotely.flow.sync.FlowTypeMetadata meta : snapshot.getTypeMetadata()) { + FlowDataType.registerServerType(meta.getId(), meta.getDisplayName(), meta.getColor(), meta.getParentId(), meta.isCanStringify()); + } + } + if (snapshot.getCategoryMetadata() != null) { + serverCategoryMetadata.put(key, new ArrayList<>(snapshot.getCategoryMetadata())); + for (redxax.oxy.remotely.flow.sync.FlowCategoryMetadata meta : snapshot.getCategoryMetadata()) { + NodeDefinition.NodeCategory.registerServerCategory(meta.getId(), meta.getDisplayName(), meta.getColor(), meta.getPriority()); + } + } + if (snapshot.getOptionSourceMetadata() != null) { + serverOptionSourceMetadata.put(key, new ArrayList<>(snapshot.getOptionSourceMetadata())); + } + if (snapshot.getConversionRules() != null) { + serverConversionRules.put(key, new ArrayList<>(snapshot.getConversionRules())); + } + rebuildServerDefinitions(key); notifyListeners(serverId); } @@ -128,9 +161,114 @@ public void clearServer(String serverId) { serverPlugins.remove(key); serverNodeIds.remove(key); serverDefinitions.remove(key); + serverPropertyActions.remove(key); + serverPropertyOutputTypes.remove(key); + serverTypeMetadata.remove(key); + serverCategoryMetadata.remove(key); + serverOptionSourceMetadata.remove(key); + serverConversionRules.remove(key); notifyListeners(serverId); } + public List getServerCategories(String serverId) { + String key = normalizeServerId(serverId); + List meta = serverCategoryMetadata.get(key); + if (meta != null && !meta.isEmpty()) { + return meta; + } + return NodeDefinition.NodeCategory.values().stream() + .map(cat -> new redxax.oxy.remotely.flow.sync.FlowCategoryMetadata(cat.getId(), cat.getDisplayName(), cat.getColor(), cat.getPriority())) + .toList(); + } + + public redxax.oxy.remotely.flow.sync.FlowOptionSourceMetadata getServerOptionSource(String serverId, String sourceId) { + if (sourceId == null) { + return null; + } + String key = normalizeServerId(serverId); + List list = serverOptionSourceMetadata.get(key); + if (list == null) { + return null; + } + for (redxax.oxy.remotely.flow.sync.FlowOptionSourceMetadata meta : list) { + if (meta != null && meta.getId() != null && meta.getId().equalsIgnoreCase(sourceId)) { + return meta; + } + } + return null; + } + + public redxax.oxy.remotely.flow.sync.FlowTypeMetadata getTypeMetadata(String serverId, String typeId) { + if (typeId == null) { + return null; + } + String key = normalizeServerId(serverId); + List list = serverTypeMetadata.get(key); + if (list == null) { + return null; + } + for (redxax.oxy.remotely.flow.sync.FlowTypeMetadata meta : list) { + if (meta != null && meta.getId() != null && meta.getId().equalsIgnoreCase(typeId)) { + return meta; + } + } + return null; + } + + public boolean canConvertTypes(String serverId, FlowDataType source, FlowDataType target) { + if (source == null || target == null) { + return false; + } + if (source.canConvertTo(target)) { + return true; + } + String key = normalizeServerId(serverId); + List rules = serverConversionRules.get(key); + if (rules == null) { + return false; + } + String sourceId = source.getId(); + String targetId = target.getId(); + for (redxax.oxy.remotely.flow.sync.FlowConversionRule rule : rules) { + if (rule != null + && rule.getSourceTypeId() != null + && rule.getTargetTypeId() != null + && rule.getSourceTypeId().equalsIgnoreCase(sourceId) + && rule.getTargetTypeId().equalsIgnoreCase(targetId)) { + return true; + } + } + return false; + } + + public List getPropertyActions(String serverId, String family, String property) { + String key = normalizeServerId(serverId); + Map>> families = serverPropertyActions.get(key); + if (families == null) { + return List.of(); + } + Map> properties = families.get(family); + if (properties == null) { + return List.of(); + } + List actions = properties.get(property); + return actions != null ? actions : List.of(); + } + + public FlowDataType getPropertyOutputType(String serverId, String family, String property) { + String key = normalizeServerId(serverId); + Map> families = serverPropertyOutputTypes.get(key); + if (families == null) { + return FlowDataType.ANY; + } + Map properties = families.get(family); + if (properties == null) { + return FlowDataType.ANY; + } + FlowDataType type = properties.get(property); + return type != null ? type : FlowDataType.ANY; + } + public void addListener(NodeRegistryListener listener) { if (listener != null) { listeners.add(listener); diff --git a/src/main/java/redxax/oxy/remotely/flow/sync/FlowCategoryMetadata.java b/src/main/java/redxax/oxy/remotely/flow/sync/FlowCategoryMetadata.java new file mode 100644 index 00000000..ec53507b --- /dev/null +++ b/src/main/java/redxax/oxy/remotely/flow/sync/FlowCategoryMetadata.java @@ -0,0 +1,50 @@ +package redxax.oxy.remotely.flow.sync; + +public class FlowCategoryMetadata { + private String id; + private String displayName; + private int color; + private int priority; + + public FlowCategoryMetadata() { + } + + public FlowCategoryMetadata(String id, String displayName, int color, int priority) { + this.id = id; + this.displayName = displayName; + this.color = color; + this.priority = priority; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public int getColor() { + return color; + } + + public void setColor(int color) { + this.color = color; + } + + public int getPriority() { + return priority; + } + + public void setPriority(int priority) { + this.priority = priority; + } +} diff --git a/src/main/java/redxax/oxy/remotely/flow/sync/FlowConversionRule.java b/src/main/java/redxax/oxy/remotely/flow/sync/FlowConversionRule.java new file mode 100644 index 00000000..d5682185 --- /dev/null +++ b/src/main/java/redxax/oxy/remotely/flow/sync/FlowConversionRule.java @@ -0,0 +1,30 @@ +package redxax.oxy.remotely.flow.sync; + +public class FlowConversionRule { + private String sourceTypeId; + private String targetTypeId; + + public FlowConversionRule() { + } + + public FlowConversionRule(String sourceTypeId, String targetTypeId) { + this.sourceTypeId = sourceTypeId; + this.targetTypeId = targetTypeId; + } + + public String getSourceTypeId() { + return sourceTypeId; + } + + public void setSourceTypeId(String sourceTypeId) { + this.sourceTypeId = sourceTypeId; + } + + public String getTargetTypeId() { + return targetTypeId; + } + + public void setTargetTypeId(String targetTypeId) { + this.targetTypeId = targetTypeId; + } +} diff --git a/src/main/java/redxax/oxy/remotely/flow/sync/FlowOptionSourceMetadata.java b/src/main/java/redxax/oxy/remotely/flow/sync/FlowOptionSourceMetadata.java new file mode 100644 index 00000000..2cc99c65 --- /dev/null +++ b/src/main/java/redxax/oxy/remotely/flow/sync/FlowOptionSourceMetadata.java @@ -0,0 +1,50 @@ +package redxax.oxy.remotely.flow.sync; + +public class FlowOptionSourceMetadata { + private String id; + private String provider; + private String widgetType; + private boolean searchable; + + public FlowOptionSourceMetadata() { + } + + public FlowOptionSourceMetadata(String id, String provider, String widgetType, boolean searchable) { + this.id = id; + this.provider = provider; + this.widgetType = widgetType; + this.searchable = searchable; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getProvider() { + return provider; + } + + public void setProvider(String provider) { + this.provider = provider; + } + + public String getWidgetType() { + return widgetType; + } + + public void setWidgetType(String widgetType) { + this.widgetType = widgetType; + } + + public boolean isSearchable() { + return searchable; + } + + public void setSearchable(boolean searchable) { + this.searchable = searchable; + } +} diff --git a/src/main/java/redxax/oxy/remotely/flow/sync/FlowTypeMetadata.java b/src/main/java/redxax/oxy/remotely/flow/sync/FlowTypeMetadata.java new file mode 100644 index 00000000..65c91c0b --- /dev/null +++ b/src/main/java/redxax/oxy/remotely/flow/sync/FlowTypeMetadata.java @@ -0,0 +1,80 @@ +package redxax.oxy.remotely.flow.sync; + +public class FlowTypeMetadata { + private String id; + private String displayName; + private int color; + private String parentId; + private boolean canStringify; + private boolean literalInput; + private boolean objectPin; + + public FlowTypeMetadata() { + } + + public FlowTypeMetadata(String id, String displayName, int color, String parentId, boolean canStringify, boolean literalInput, boolean objectPin) { + this.id = id; + this.displayName = displayName; + this.color = color; + this.parentId = parentId; + this.canStringify = canStringify; + this.literalInput = literalInput; + this.objectPin = objectPin; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public int getColor() { + return color; + } + + public void setColor(int color) { + this.color = color; + } + + public String getParentId() { + return parentId; + } + + public void setParentId(String parentId) { + this.parentId = parentId; + } + + public boolean isCanStringify() { + return canStringify; + } + + public void setCanStringify(boolean canStringify) { + this.canStringify = canStringify; + } + + public boolean isLiteralInput() { + return literalInput; + } + + public void setLiteralInput(boolean literalInput) { + this.literalInput = literalInput; + } + + public boolean isObjectPin() { + return objectPin; + } + + public void setObjectPin(boolean objectPin) { + this.objectPin = objectPin; + } +} diff --git a/src/main/java/redxax/oxy/remotely/flow/sync/NodeRegistrySnapshot.java b/src/main/java/redxax/oxy/remotely/flow/sync/NodeRegistrySnapshot.java index 7f52c91b..103c11e6 100644 --- a/src/main/java/redxax/oxy/remotely/flow/sync/NodeRegistrySnapshot.java +++ b/src/main/java/redxax/oxy/remotely/flow/sync/NodeRegistrySnapshot.java @@ -1,13 +1,22 @@ package redxax.oxy.remotely.flow.sync; +import redxax.oxy.remotely.flow.data.FlowDataType; + import java.util.ArrayList; import java.util.List; +import java.util.Map; public class NodeRegistrySnapshot { private boolean fullSync; private List nodeIds = new ArrayList<>(); private List plugins = new ArrayList<>(); private List removedPlugins = new ArrayList<>(); + private Map>> propertyActions; + private Map> propertyOutputTypes; + private List typeMetadata = new ArrayList<>(); + private List categoryMetadata = new ArrayList<>(); + private List optionSourceMetadata = new ArrayList<>(); + private List conversionRules = new ArrayList<>(); public boolean isFullSync() { return fullSync; @@ -40,4 +49,52 @@ public List getRemovedPlugins() { public void setRemovedPlugins(List removedPlugins) { this.removedPlugins = removedPlugins != null ? removedPlugins : new ArrayList<>(); } + + public Map>> getPropertyActions() { + return propertyActions; + } + + public void setPropertyActions(Map>> propertyActions) { + this.propertyActions = propertyActions; + } + + public Map> getPropertyOutputTypes() { + return propertyOutputTypes; + } + + public void setPropertyOutputTypes(Map> propertyOutputTypes) { + this.propertyOutputTypes = propertyOutputTypes; + } + + public List getTypeMetadata() { + return typeMetadata; + } + + public void setTypeMetadata(List typeMetadata) { + this.typeMetadata = typeMetadata != null ? typeMetadata : new ArrayList<>(); + } + + public List getCategoryMetadata() { + return categoryMetadata; + } + + public void setCategoryMetadata(List categoryMetadata) { + this.categoryMetadata = categoryMetadata != null ? categoryMetadata : new ArrayList<>(); + } + + public List getOptionSourceMetadata() { + return optionSourceMetadata; + } + + public void setOptionSourceMetadata(List optionSourceMetadata) { + this.optionSourceMetadata = optionSourceMetadata != null ? optionSourceMetadata : new ArrayList<>(); + } + + public List getConversionRules() { + return conversionRules; + } + + public void setConversionRules(List conversionRules) { + this.conversionRules = conversionRules != null ? conversionRules : new ArrayList<>(); + } } diff --git a/src/main/java/redxax/oxy/remotely/flow/ui/FlowEditorScreen.java b/src/main/java/redxax/oxy/remotely/flow/ui/FlowEditorScreen.java index 57fa4156..37839644 100644 --- a/src/main/java/redxax/oxy/remotely/flow/ui/FlowEditorScreen.java +++ b/src/main/java/redxax/oxy/remotely/flow/ui/FlowEditorScreen.java @@ -4,7 +4,7 @@ import redxax.oxy.remotely.flow.data.FlowConnection; import redxax.oxy.remotely.flow.data.FlowGraph; import redxax.oxy.remotely.flow.data.FlowNode; -import redxax.oxy.remotely.flow.data.FlowType; +import redxax.oxy.remotely.flow.data.FlowDataType; import redxax.oxy.remotely.flow.registry.NodeDefinition; import redxax.oxy.remotely.flow.registry.NodeRegistry; import org.lwjgl.glfw.GLFW; @@ -51,25 +51,7 @@ public class FlowEditorScreen extends InfiniteScreen implements UiHost { private SidePanel paletteSidePanel; private final Map categoryPopups = new HashMap<>(); - private static final List CATEGORY_ORDER = List.of( - NodeDefinition.NodeCategory.EVENT, - NodeDefinition.NodeCategory.ACTION, - NodeDefinition.NodeCategory.LOGIC, - NodeDefinition.NodeCategory.DATA, - NodeDefinition.NodeCategory.VARIABLE, - NodeDefinition.NodeCategory.FUNCTION, - NodeDefinition.NodeCategory.ENTITY, - NodeDefinition.NodeCategory.WORLD, - NodeDefinition.NodeCategory.INVENTORY, - NodeDefinition.NodeCategory.SCOREBOARD, - NodeDefinition.NodeCategory.ECONOMY, - NodeDefinition.NodeCategory.PERMISSION, - NodeDefinition.NodeCategory.VISUAL, - NodeDefinition.NodeCategory.UTILITY, - NodeDefinition.NodeCategory.DATABASE, - NodeDefinition.NodeCategory.HTTP, - NodeDefinition.NodeCategory.DISCORD - ); + private List categoryOrder = List.of(); private IconButton headerBackground; private final List headerButtons = new ArrayList<>(); @@ -354,7 +336,8 @@ private void createPaletteSidePanel() { paletteSidePanel.container().layout(new ManagedLayout()).columns(1).padding(5); categoryPopups.clear(); - for (NodeDefinition.NodeCategory category : CATEGORY_ORDER) { + categoryOrder = resolveCategoryOrder(); + for (NodeDefinition.NodeCategory category : categoryOrder) { PopupWidget popup = new PopupWidget.Builder(getCategoryLabel(category)).enableCollapseOnClose(true).build(); popup.collapse(true); categoryPopups.put(category, popup); @@ -369,8 +352,9 @@ private void createPaletteSidePanel() { } private void populateCategoryPopups() { + List order = categoryOrder.isEmpty() ? resolveCategoryOrder() : categoryOrder; Map> categories = new HashMap<>(); - for (NodeDefinition.NodeCategory category : CATEGORY_ORDER) { + for (NodeDefinition.NodeCategory category : order) { categories.put(category, new ArrayList<>()); } @@ -389,7 +373,7 @@ private void populateCategoryPopups() { .comparingInt(NodeDefinition::getPriority) .thenComparing(NodeDefinition::getDisplayName, String.CASE_INSENSITIVE_ORDER); - for (NodeDefinition.NodeCategory category : CATEGORY_ORDER) { + for (NodeDefinition.NodeCategory category : order) { PopupWidget targetPopup = getCategoryPopup(category); if (targetPopup == null) { continue; @@ -407,6 +391,15 @@ private void populateCategoryPopups() { } } + private List resolveCategoryOrder() { + List meta = NodeRegistry.getInstance().getServerCategories(serverId); + List result = new ArrayList<>(); + for (redxax.oxy.remotely.flow.sync.FlowCategoryMetadata m : meta) { + result.add(NodeDefinition.NodeCategory.fromString(m.getId())); + } + return result; + } + private void populateFallbackPopups() { PopupWidget eventsPopup = getCategoryPopup(NodeDefinition.NodeCategory.EVENT); PopupWidget actionsPopup = getCategoryPopup(NodeDefinition.NodeCategory.ACTION); @@ -432,25 +425,7 @@ private PopupWidget getCategoryPopup(NodeDefinition.NodeCategory category) { } private String getCategoryLabel(NodeDefinition.NodeCategory category) { - return switch (category) { - case EVENT -> "Events"; - case ACTION -> "Actions"; - case LOGIC -> "Logic"; - case DATA -> "Data"; - case VARIABLE -> "Variables"; - case FUNCTION -> "Functions"; - case ENTITY -> "Entities"; - case WORLD -> "World"; - case INVENTORY -> "Inventory"; - case SCOREBOARD -> "Scoreboard"; - case ECONOMY -> "Economy"; - case PERMISSION -> "Permissions"; - case VISUAL -> "Visual"; - case UTILITY -> "Utility"; - case DATABASE -> "Database"; - case HTTP -> "HTTP"; - case DISCORD -> "Discord"; - }; + return category.getDisplayName(); } private void addNodeAtCenter(String type) { @@ -654,7 +629,7 @@ private boolean extractSelectionToFunction(String functionId) { } String parameterName = uniqueParameterName(connection.targetPin(), usedInputNames); usedInputNames.add(parameterName); - FlowType parameterType = resolveTargetPinType(connection.targetNodeId(), connection.targetPin()); + FlowDataType parameterType = resolveTargetPinType(connection.targetNodeId(), connection.targetPin()); functionGraph.getFunctionInputs().add(new FlowGraph.FunctionParameter(parameterName, parameterType)); functionGraph.getConnections().add(new FlowConnection(functionStartId, parameterName, connection.targetNodeId(), connection.targetPin())); inboundParams.put(connection, parameterName); @@ -666,7 +641,7 @@ private boolean extractSelectionToFunction(String functionId) { } String parameterName = uniqueParameterName(connection.sourcePin(), usedOutputNames); usedOutputNames.add(parameterName); - FlowType parameterType = resolveSourcePinType(connection.sourceNodeId(), connection.sourcePin()); + FlowDataType parameterType = resolveSourcePinType(connection.sourceNodeId(), connection.sourcePin()); functionGraph.getFunctionOutputs().add(new FlowGraph.FunctionParameter(parameterName, parameterType)); functionGraph.getConnections().add(new FlowConnection(connection.sourceNodeId(), connection.sourcePin(), functionEndId, parameterName)); outboundParams.put(connection, parameterName); @@ -754,39 +729,45 @@ private String uniqueParameterName(String baseName, Set usedNames) { return candidate; } - private FlowType resolveTargetPinType(String nodeId, String pinName) { + private FlowDataType resolveTargetPinType(String nodeId, String pinName) { + if (nodeId == null || pinName == null || nodeId.isBlank() || pinName.isBlank()) { + return FlowDataType.ANY; + } NodeWidget widget = widgetCache.get(nodeId); if (widget == null) { - return FlowType.ANY; + return FlowDataType.ANY; } - FlowType type = widget.getPinType(pinName, true); - return type != null ? type : FlowType.ANY; + FlowDataType type = widget.getPinType(pinName, true); + return type != null ? type : FlowDataType.ANY; } - private FlowType resolveSourcePinType(String nodeId, String pinName) { + private FlowDataType resolveSourcePinType(String nodeId, String pinName) { + if (nodeId == null || pinName == null || nodeId.isBlank() || pinName.isBlank()) { + return FlowDataType.ANY; + } NodeWidget widget = widgetCache.get(nodeId); if (widget == null) { - return FlowType.ANY; + return FlowDataType.ANY; } - FlowType type = widget.getPinType(pinName, false); - return type != null ? type : FlowType.ANY; + FlowDataType type = widget.getPinType(pinName, false); + return type != null ? type : FlowDataType.ANY; } private NodeDefinition buildCustomFunctionNodeDefinition(String nodeType, String functionId, FlowGraph functionGraph) { NodeDefinition.Builder builder = new NodeDefinition.Builder(nodeType, formatFunctionDisplayName(functionId), NodeDefinition.NodeCategory.FUNCTION); - builder.input("flow", NodeDefinition.PinType.FLOW, FlowType.EXECUTION); - builder.output("flow", NodeDefinition.PinType.FLOW, FlowType.EXECUTION); + builder.input("flow", NodeDefinition.PinType.FLOW, FlowDataType.EXECUTION); + builder.output("flow", NodeDefinition.PinType.FLOW, FlowDataType.EXECUTION); if (functionGraph.getFunctionInputs() != null) { for (FlowGraph.FunctionParameter param : functionGraph.getFunctionInputs()) { if (param != null && param.getName() != null && !param.getName().isBlank()) { - builder.input(param.getName(), NodeDefinition.PinType.DATA, param.getType() != null ? param.getType() : FlowType.ANY); + builder.input(param.getName(), NodeDefinition.PinType.DATA, param.getType() != null ? param.getType() : FlowDataType.ANY); } } } if (functionGraph.getFunctionOutputs() != null) { for (FlowGraph.FunctionParameter param : functionGraph.getFunctionOutputs()) { if (param != null && param.getName() != null && !param.getName().isBlank()) { - builder.output(param.getName(), NodeDefinition.PinType.DATA, param.getType() != null ? param.getType() : FlowType.ANY); + builder.output(param.getName(), NodeDefinition.PinType.DATA, param.getType() != null ? param.getType() : FlowDataType.ANY); } } } @@ -914,7 +895,7 @@ private void renderWires(IDrawContext context) { float endX = (float) (end[0] + end[2]/2); float endY = (float) (end[1] + end[3]/2); - FlowType sourceType = source.getPinType(conn.getSourcePin(), false); + FlowDataType sourceType = source.getPinType(conn.getSourcePin(), false); int wireColor = (sourceType != null) ? sourceType.getColor() : ThemeManager.getColor(ThemeColor.innerBorder); int laneOffset = getWireLaneOffset(conn); drawWire(context, startX, startY, endX, endY, wireColor, laneOffset); @@ -925,7 +906,7 @@ private void renderWires(IDrawContext context) { if (dragState.isDragging && dragState.sourceNodeId != null) { double[] sourcePinWorld = null; NodeWidget source = widgetCache.get(dragState.sourceNodeId); - FlowType sourceType = null; + FlowDataType sourceType = null; if (source != null) { double[] bounds = source.getPinBounds(dragState.sourcePin, dragState.sourceIsInput); if (bounds != null) { @@ -1520,8 +1501,8 @@ private void renderSelectionBox(IDrawContext context) { } private boolean canConnect(NodeWidget sourceWidget, String sourcePin, NodeWidget targetWidget, String targetPin) { - FlowType sourceType = sourceWidget.getPinType(sourcePin, false); - FlowType targetType = targetWidget.getPinType(targetPin, true); + FlowDataType sourceType = sourceWidget.getPinType(sourcePin, false); + FlowDataType targetType = targetWidget.getPinType(targetPin, true); NodeDefinition.PinType sourceKind = sourceWidget.getPinKind(sourcePin, false); NodeDefinition.PinType targetKind = targetWidget.getPinKind(targetPin, true); @@ -1530,14 +1511,24 @@ private boolean canConnect(NodeWidget sourceWidget, String sourcePin, NodeWidget } if (sourceKind == NodeDefinition.PinType.FLOW || targetKind == NodeDefinition.PinType.FLOW) { - return sourceKind == NodeDefinition.PinType.FLOW && targetKind == NodeDefinition.PinType.FLOW && sourceType == FlowType.EXECUTION && targetType == FlowType.EXECUTION; + return sourceKind == NodeDefinition.PinType.FLOW && targetKind == NodeDefinition.PinType.FLOW && sourceType == FlowDataType.EXECUTION && targetType == FlowDataType.EXECUTION; } if (sourceKind != NodeDefinition.PinType.DATA || targetKind != NodeDefinition.PinType.DATA) { return false; } - return sourceType.isCompatibleWith(targetType); + return isTypeCompatible(sourceType, targetType); + } + + private boolean isTypeCompatible(FlowDataType sourceType, FlowDataType targetType) { + if (sourceType == null || targetType == null) { + return false; + } + if (sourceType.canConvertTo(targetType)) { + return true; + } + return NodeRegistry.getInstance().canConvertTypes(serverId, sourceType, targetType); } private void tryCompleteWire(double worldMouseX, double worldMouseY, double screenMouseX, double screenMouseY) { @@ -1582,7 +1573,7 @@ private void tryCompleteWire(double worldMouseX, double worldMouseY, double scre } if (!connected && dragPinWidget != null) { - FlowType sourceType = dragPinWidget.getPinType(dragState.sourcePin, dragState.sourceIsInput); + FlowDataType sourceType = dragPinWidget.getPinType(dragState.sourcePin, dragState.sourceIsInput); if (sourceType != null) { pendingSourceNodeId = dragState.sourceNodeId; pendingSourcePin = dragState.sourcePin; @@ -1620,7 +1611,7 @@ private String findNodeId(NodeWidget widget) { return null; } - private void showAddNodeMenu(int x, int y, FlowType sourceType, boolean sourceIsInput) { + private void showAddNodeMenu(int x, int y, FlowDataType sourceType, boolean sourceIsInput) { if (nodeItemSelector != null) { remove(nodeItemSelector); nodeItemSelector = null; @@ -1663,16 +1654,19 @@ private void showAddNodeMenu(int x, int y, FlowType sourceType, boolean sourceIs nodeItemSelector.show(x, y); } - private String findCompatiblePin(NodeDefinition definition, FlowType sourceType, boolean sourceIsInput) { + private String findCompatiblePin(NodeDefinition definition, FlowDataType sourceType, boolean sourceIsInput) { List pins = sourceIsInput ? definition.getOutputs() : definition.getInputs(); for (NodeDefinition.PinDefinition pin : pins) { - if (sourceType == FlowType.EXECUTION) { - if (pin.getType() == NodeDefinition.PinType.FLOW && pin.getDataType() == FlowType.EXECUTION) { + if (pin.getVisibleWhen() != null && !pin.getVisibleWhen().isEmpty()) { + continue; + } + if (sourceType == FlowDataType.EXECUTION) { + if (pin.getType() == NodeDefinition.PinType.FLOW && pin.getDataType() == FlowDataType.EXECUTION) { return pin.getName(); } continue; } - if (pin.getType() == NodeDefinition.PinType.DATA && sourceType.isCompatibleWith(pin.getDataType())) { + if (pin.getType() == NodeDefinition.PinType.DATA && isTypeCompatible(sourceType, pin.getDataType())) { return pin.getName(); } } diff --git a/src/main/java/redxax/oxy/remotely/flow/ui/NodeWidget.java b/src/main/java/redxax/oxy/remotely/flow/ui/NodeWidget.java index f3289fef..8b6f8ef2 100644 --- a/src/main/java/redxax/oxy/remotely/flow/ui/NodeWidget.java +++ b/src/main/java/redxax/oxy/remotely/flow/ui/NodeWidget.java @@ -3,9 +3,11 @@ import redxax.oxy.remotely.flow.data.FlowConnection; import redxax.oxy.remotely.flow.data.FlowGraph; import redxax.oxy.remotely.flow.data.FlowNode; -import redxax.oxy.remotely.flow.data.FlowType; +import redxax.oxy.remotely.flow.data.FlowDataType; import redxax.oxy.remotely.flow.registry.NodeDefinition; import redxax.oxy.remotely.flow.registry.NodeRegistry; +import redxax.oxy.remotely.flow.sync.FlowOptionSourceMetadata; +import redxax.oxy.remotely.flow.sync.FlowTypeMetadata; import restudio.rescreen.platform.IDrawContext; import restudio.rescreen.platform.ITextRenderer; import restudio.rescreen.render.Render; @@ -15,16 +17,24 @@ import restudio.rescreen.ui.core.ScreenManager; import restudio.rescreen.ui.widgets.AnimatedButton; import restudio.rescreen.ui.widgets.AnimatedWidget; +import restudio.rescreen.ui.widgets.ColorFieldWidget; import restudio.rescreen.ui.widgets.ContextMenuWidget; import restudio.rescreen.ui.widgets.DropDownWidget; +import restudio.rescreen.ui.widgets.ItemSelectorWidget; import restudio.rescreen.ui.widgets.PopupWidget; +import restudio.rescreen.ui.widgets.SliderWidget; +import restudio.rebase.minecraft.assets.MinecraftAssetsManager; +import restudio.rebase.minecraft.assets.MinecraftCatalog; +import restudio.rebase.ui.widgets.editor.TextAreaWidget; import restudio.rescreen.ui.widgets.TextInputWidget; import restudio.rescreen.ui.widgets.ToggleWidget; import java.util.ArrayList; import java.util.HashMap; +import java.util.Set; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; import static restudio.rescreen.config.Config.shadow; import static restudio.rescreen.render.TextRenderer.tr; @@ -38,6 +48,7 @@ public class NodeWidget extends AnimatedWidget { private final List outputs = new ArrayList<>(); private final NodeDefinition definition; private final Map inputWidgets = new HashMap<>(); + private final List visibleInputs = new ArrayList<>(); private final List visibleOutputs = new ArrayList<>(); private final List flowBranches = new ArrayList<>(); private final Runnable onClose; @@ -65,6 +76,7 @@ public class NodeWidget extends AnimatedWidget { private static final String FLOW_BRANCHES_KEY = "__flow_branches"; private static final String FUNCTION_START_ID = "function_start"; private static final String FUNCTION_END_ID = "function_end"; + private boolean updatingBranchSelection = false; private int lastScreenX; private int lastScreenY; @@ -120,6 +132,7 @@ public NodeWidget(int x, int y, FlowNode node, FlowGraph graph, String nodeId, S inputs.addAll(definition.getInputs()); outputs.addAll(definition.getOutputs()); applyFunctionParameterPins(); + seedDefaultInputValues(); createInputWidgets(); createOutputWidgets(); updateSize(); @@ -129,71 +142,303 @@ public NodeWidget(int x, int y, FlowNode node, FlowGraph graph, String nodeId, S } private void createInputWidgets() { - if (graph == null || nodeId == null || graph.getConnections() == null) return; + if (graph == null || nodeId == null) return; for (NodeDefinition.PinDefinition input : inputs) { if (input.getType() != NodeDefinition.PinType.DATA) { continue; } - if (!isLiteralType(input.getDataType())) { + if (!isLiteralInput(input)) { + continue; + } + if (isInputWired(input.getName())) { continue; } - if (!isInputWired(input.getName())) { - Object currentValue = node.getInputValues() != null ? node.getInputValues().get(input.getName()) : null; - List options = getDropdownOptions(input); + Widget widget = buildWidgetForPin(input); + if (widget != null) { + inputWidgets.put(input.getName(), widget); + } + } + updatePinVisibility(); + } - if (options != null) { - String selected = currentValue != null ? currentValue.toString() : options.getFirst(); - for (String option : options) { - if (option.equalsIgnoreCase(selected)) { - selected = option; - break; - } + private Widget buildWidgetForPin(NodeDefinition.PinDefinition input) { + Object currentValue = node.getInputValues() != null ? node.getInputValues().get(input.getName()) : null; + NodeDefinition.WidgetType widgetType = resolveWidgetType(input); + + switch (widgetType) { + case DROPDOWN -> { + List options = resolveOptions(input); + if (options.isEmpty()) { + return buildTextInput(currentValue, input.getOptionsSource()); + } + String selected = resolveSelected(options, currentValue, input.getDefaultValue()); + if (options.size() > 20) { + return buildSearchableSelector(input, options, selected); + } + return new DropDownWidget.Builder<>(options) + .selectedItem(selected) + .onSelectionChanged(value -> { + saveInputValue(); + updatePinVisibility(); + }) + .size(INPUT_WIDGET_WIDTH, INPUT_WIDGET_HEIGHT) + .entranceAnimation(false) + .build(); + } + case SEARCHABLE_LIST -> { + List options = resolveOptions(input); + if (options.isEmpty()) { + return buildTextInput(currentValue, input.getOptionsSource()); + } + String selected = resolveSelected(options, currentValue, input.getDefaultValue()); + return buildSearchableSelector(input, options, selected); + } + case TOGGLE -> { + boolean toggled = currentValue instanceof Boolean ? (boolean) currentValue : Boolean.parseBoolean(String.valueOf(currentValue)); + ToggleWidget widget = new ToggleWidget.Builder() + .toggled(toggled) + .onChange(() -> { + saveInputValue(); + updatePinVisibility(); + }) + .entranceAnimation(false) + .build(); + widget.setSize(TOGGLE_WIDGET_WIDTH, TOGGLE_WIDGET_HEIGHT); + return widget; + } + case SLIDER -> { + double value = 0.0; + if (currentValue instanceof Number n) { + value = n.doubleValue(); + } else if (currentValue != null) { + try { + value = Double.parseDouble(currentValue.toString()); + } catch (NumberFormatException ignored) { } - DropDownWidget widget = new DropDownWidget.Builder<>(options) - .selectedItem(selected) - .onSelectionChanged(value -> saveInputValue()) - .size(INPUT_WIDGET_WIDTH, INPUT_WIDGET_HEIGHT) - .entranceAnimation(false) - .build(); - inputWidgets.put(input.getName(), widget); - } else if (input.getDataType() == FlowType.BOOLEAN) { - boolean toggled = currentValue instanceof Boolean ? (boolean) currentValue : Boolean.parseBoolean(String.valueOf(currentValue)); - ToggleWidget widget = new ToggleWidget.Builder() - .toggled(toggled) - .onChange(this::saveInputValue) - .entranceAnimation(false) - .build(); - widget.setSize(TOGGLE_WIDGET_WIDTH, TOGGLE_WIDGET_HEIGHT); - inputWidgets.put(input.getName(), widget); - } else { - String textValue = currentValue != null ? currentValue.toString() : ""; - TextInputWidget widget = new TextInputWidget.Builder() - .text(textValue) - .placeholder("") - .forcePlaceholder(false) - .size(INPUT_WIDGET_WIDTH, INPUT_WIDGET_HEIGHT) - .onChange(this::saveInputValue) - .entranceAnimation(false) - .build(); - inputWidgets.put(input.getName(), widget); } + NodeDefinition.PinConstraints constraints = input.getConstraints(); + double min = constraints != null && constraints.getMin() != null ? constraints.getMin() : 0.0; + double max = constraints != null && constraints.getMax() != null ? constraints.getMax() : 100.0; + double step = constraints != null && constraints.getStep() != null ? constraints.getStep() : 1.0; + return new SliderWidget.Builder() + .min(min) + .max(max) + .step(step) + .value(value) + .onChange(() -> { + saveInputValue(); + updatePinVisibility(); + }) + .size(INPUT_WIDGET_WIDTH, INPUT_WIDGET_HEIGHT) + .entranceAnimation(false) + .build(); + } + case NUMBER -> { + String textValue = currentValue != null ? currentValue.toString() : ""; + TextInputWidget widget = new TextInputWidget.Builder() + .text(textValue) + .placeholder("") + .forcePlaceholder(false) + .numericOnly(true) + .size(INPUT_WIDGET_WIDTH, INPUT_WIDGET_HEIGHT) + .onChange(this::saveInputValue) + .entranceAnimation(false) + .build(); + return widget; + } + case MULTILINE -> { + String textValue = currentValue != null ? currentValue.toString() : ""; + return new TextAreaWidget.Builder() + .text(textValue) + .placeholder("") + .size(INPUT_WIDGET_WIDTH, INPUT_WIDGET_HEIGHT * 3) + .onChange(text -> saveInputValue()) + .entranceAnimation(false) + .build(); + } + case COLOR -> { + String textValue = currentValue != null ? currentValue.toString() : "#FFFFFF"; + return new ColorFieldWidget.Builder() + .color(textValue) + .onChange(() -> { + saveInputValue(); + updatePinVisibility(); + }) + .size(INPUT_WIDGET_WIDTH, INPUT_WIDGET_HEIGHT) + .entranceAnimation(false) + .build(); + } + default -> { + return buildTextInput(currentValue, ""); + } + } + } + + private Widget buildTextInput(Object currentValue, String placeholder) { + String textValue = currentValue != null ? currentValue.toString() : ""; + return new TextInputWidget.Builder() + .text(textValue) + .placeholder(placeholder != null ? placeholder : "") + .forcePlaceholder(false) + .size(INPUT_WIDGET_WIDTH, INPUT_WIDGET_HEIGHT) + .onChange(this::saveInputValue) + .entranceAnimation(false) + .build(); + } + + private Widget buildSearchableSelector(NodeDefinition.PinDefinition input, List options, String selected) { + AnimatedButton button = new AnimatedButton.Builder() + .label(selected) + .size(INPUT_WIDGET_WIDTH, INPUT_WIDGET_HEIGHT) + .entranceAnimation(false) + .build(); + button.setAction(() -> { + var screen = ScreenManager.getInstance().getCurrentScreen(); + if (screen == null) return; + AtomicReference selector = new AtomicReference<>(); + selector.set(new ItemSelectorWidget.Builder(screen) + .size(180, 220) + .dismissOnSelect(true) + .onClose(() -> screen.remove(selector.get())) + .build()); + for (String option : options) { + selector.get().addItem(option, () -> { + node.getInputValues().put(input.getName(), option); + button.setMessage(option); + saveInputValue(); + updatePinVisibility(); + }); + } + selector.get().setSelectedItem(selected); + screen.addDrawableChild(selector.get()); + selector.get().show(button.getX(), button.getY() + button.getHeight()); + }); + return button; + } + + private List resolveOptions(NodeDefinition.PinDefinition input) { + List options = input.getOptions(); + if (options != null && !options.isEmpty()) { + return options; + } + String catalog = resolveMinecraftCatalog(input.getOptionsSource()); + if (catalog != null) { + return MinecraftCatalog.getCatalog(MinecraftAssetsManager.getInstance(), "minecraft", catalog); + } + return List.of(); + } + + private String resolveSelected(List options, Object currentValue, String defaultValue) { + String selected = currentValue != null ? currentValue.toString() : defaultValue; + if (selected == null || selected.isBlank()) { + selected = options.isEmpty() ? "" : options.getFirst(); + } + for (String option : options) { + if (option.equalsIgnoreCase(selected)) { + return option; } } + return selected; } - private List getDropdownOptions(NodeDefinition.PinDefinition input) { - if (definition == null) { + private NodeDefinition.WidgetType resolveWidgetType(NodeDefinition.PinDefinition input) { + if (input.getWidgetType() != null && input.getWidgetType() != NodeDefinition.WidgetType.AUTO) { + return input.getWidgetType(); + } + String optionsSource = input.getOptionsSource(); + if (optionsSource != null && !optionsSource.isBlank()) { + NodeRegistry registry = NodeRegistry.getInstance(); + FlowOptionSourceMetadata meta = registry != null ? registry.getServerOptionSource(serverId, optionsSource) : null; + if (meta != null) { + return meta.isSearchable() ? NodeDefinition.WidgetType.SEARCHABLE_LIST : NodeDefinition.WidgetType.DROPDOWN; + } + String catalog = resolveMinecraftCatalog(optionsSource); + if (catalog != null) { + return NodeDefinition.WidgetType.DROPDOWN; + } + return NodeDefinition.WidgetType.DROPDOWN; + } + if (input.getDataType() == FlowDataType.BOOLEAN) { + return NodeDefinition.WidgetType.TOGGLE; + } + return NodeDefinition.WidgetType.TEXT; + } + + private String resolveMinecraftCatalog(String optionsSource) { + if (optionsSource == null || optionsSource.isBlank()) { return null; } - if ("variable_access".equals(definition.getId())) { - return switch (input.getName()) { - case "mode" -> List.of("Get", "Set", "Exists", "Delete", "List", "Increment", "Decrement", "Multiply", "Divide"); - case "scope" -> List.of("Local", "Global", "Player"); - default -> null; - }; + String catalog = null; + if (optionsSource.startsWith("client:minecraft:")) { + catalog = optionsSource.substring("client:minecraft:".length()); + } else if (optionsSource.startsWith("minecraft:")) { + catalog = optionsSource.substring("minecraft:".length()); } - return null; + if (catalog == null) { + return null; + } + NodeRegistry registry = NodeRegistry.getInstance(); + FlowOptionSourceMetadata meta = registry != null ? registry.getServerOptionSource(serverId, optionsSource) : null; + if (meta != null) { + return catalog; + } + return MinecraftCatalog.getCatalog(MinecraftAssetsManager.getInstance(), "minecraft", catalog) != null ? catalog : null; + } + + private void seedDefaultInputValues() { + if (node.getInputValues() == null) { + node.setInputValues(new HashMap<>()); + } + for (NodeDefinition.PinDefinition input : inputs) { + if (input.getDefaultValue() != null && !node.getInputValues().containsKey(input.getName())) { + node.getInputValues().put(input.getName(), convertValue(input.getDefaultValue(), input.getDataType())); + } + } + } + + private void updatePinVisibility() { + visibleInputs.clear(); + if (node.getInputValues() == null) { + visibleInputs.addAll(inputs); + createOutputWidgets(); + return; + } + for (NodeDefinition.PinDefinition input : inputs) { + boolean shouldShow = evaluateVisibleWhen(input.getVisibleWhen()); + Widget widget = inputWidgets.get(input.getName()); + if (widget != null) { + widget.setVisible(shouldShow); + } + if (shouldShow) { + visibleInputs.add(input); + } + } + createOutputWidgets(); + updateSize(); + updateInputWidgetPositions(); + updateOutputWidgetPositions(); + } + + private boolean evaluateVisibleWhen(Map visibleWhen) { + if (visibleWhen == null || visibleWhen.isEmpty()) { + return true; + } + for (Map.Entry condition : visibleWhen.entrySet()) { + Object actualValue = node.getInputValues().get(condition.getKey()); + String actual = actualValue != null ? actualValue.toString() : ""; + boolean matches = false; + for (String expected : condition.getValue().split(",")) { + if (actual.equalsIgnoreCase(expected.trim())) { + matches = true; + break; + } + } + if (!matches) { + return false; + } + } + return true; } public void refreshInputWidgets() { @@ -211,8 +456,8 @@ private void applyFunctionParameterPins() { inputs.clear(); outputs.clear(); - NodeDefinition.PinDefinition inputFlowPin = new NodeDefinition.PinDefinition("flow", NodeDefinition.PinType.FLOW, NodeDefinition.PinDirection.INPUT, FlowType.EXECUTION); - NodeDefinition.PinDefinition outputFlowPin = new NodeDefinition.PinDefinition("flow", NodeDefinition.PinType.FLOW, NodeDefinition.PinDirection.OUTPUT, FlowType.EXECUTION); + NodeDefinition.PinDefinition inputFlowPin = new NodeDefinition.PinDefinition("flow", NodeDefinition.PinType.FLOW, NodeDefinition.PinDirection.INPUT, FlowDataType.EXECUTION); + NodeDefinition.PinDefinition outputFlowPin = new NodeDefinition.PinDefinition("flow", NodeDefinition.PinType.FLOW, NodeDefinition.PinDirection.OUTPUT, FlowDataType.EXECUTION); inputs.add(inputFlowPin); outputs.add(outputFlowPin); @@ -252,20 +497,8 @@ private boolean isValidFunctionParameter(FlowGraph.FunctionParameter parameter) return parameter != null && parameter.getName() != null && !parameter.getName().isBlank(); } - private List getSupportedFunctionTypes() { - return List.of( - FlowType.ANY, - FlowType.STRING, - FlowType.NUMBER, - FlowType.BOOLEAN, - FlowType.PLAYER, - FlowType.ENTITY, - FlowType.LOCATION, - FlowType.ITEM, - FlowType.ITEMSTACK, - FlowType.LIST, - FlowType.JSON_OBJECT - ); + private List getSupportedFunctionTypes() { + return FlowDataType.values(); } public List getFunctionParameterList() { @@ -312,10 +545,10 @@ public void showAddFunctionParameterPopup() { .placeholder("parameter_name") .size(200, 20) .build(); - List types = getSupportedFunctionTypes(); - DropDownWidget typeDropdown = new DropDownWidget.Builder<>(types) - .selectedItem(FlowType.ANY) - .size(200, 18) + List types = getSupportedFunctionTypes(); + DropDownWidget typeDropdown = new DropDownWidget.Builder<>(types) + .selectedItem(FlowDataType.ANY) + .size(200, INPUT_WIDGET_HEIGHT) .build(); builder.addRow("Name", true, 20, nameInput); @@ -329,9 +562,9 @@ public void showAddFunctionParameterPopup() { if (rawName.isBlank() || !rawName.matches("^[a-zA-Z0-9_]+$")) { return; } - FlowType type = typeDropdown.getSelectedItem(); + FlowDataType type = typeDropdown.getSelectedItem(); if (type == null) { - type = FlowType.ANY; + type = FlowDataType.ANY; } List targetList; @@ -423,11 +656,54 @@ public void removeFunctionParameter(String name) { } } - private boolean isLiteralType(FlowType type) { - return type == FlowType.STRING || type == FlowType.NUMBER || type == FlowType.BOOLEAN || type == FlowType.ANY; + private boolean isLiteralInput(NodeDefinition.PinDefinition input) { + FlowDataType type = input.getDataType(); + if (type == null) { + return false; + } + if (isObjectPin(type) && (input.getWidgetType() == null || input.getWidgetType() == NodeDefinition.WidgetType.AUTO)) { + return false; + } + if (input.getWidgetType() != null && input.getWidgetType() != NodeDefinition.WidgetType.AUTO) { + return true; + } + if (input.getOptions() != null && !input.getOptions().isEmpty()) { + return true; + } + if (input.getOptionsSource() != null && !input.getOptionsSource().isBlank()) { + return true; + } + NodeRegistry registry = NodeRegistry.getInstance(); + FlowTypeMetadata meta = registry != null ? registry.getTypeMetadata(serverId, type.getId()) : null; + if (meta != null) { + return meta.isLiteralInput(); + } + return type == FlowDataType.STRING || type == FlowDataType.NUMBER || type == FlowDataType.BOOLEAN || type == FlowDataType.ANY; + } + + private boolean isObjectPin(FlowDataType type) { + if (type == null) { + return false; + } + NodeRegistry registry = NodeRegistry.getInstance(); + FlowTypeMetadata meta = registry != null ? registry.getTypeMetadata(serverId, type.getId()) : null; + if (meta != null) { + return meta.isObjectPin(); + } + return type == FlowDataType.PLAYER + || type == FlowDataType.ENTITY + || type == FlowDataType.LIVING_ENTITY + || type == FlowDataType.WORLD + || type == FlowDataType.BLOCK + || type == FlowDataType.LOCATION + || type == FlowDataType.INVENTORY + || type == FlowDataType.ITEMSTACK; } private boolean isInputWired(String pinName) { + if (graph == null || graph.getConnections() == null) { + return false; + } for (FlowConnection conn : graph.getConnections()) { if (conn.getTargetNodeId().equals(nodeId) && conn.getTargetPin().equals(pinName)) { return true; @@ -439,14 +715,16 @@ private boolean isInputWired(String pinName) { private void createDefaultPins() { inputs.clear(); outputs.clear(); - inputs.add(new NodeDefinition.PinDefinition("in", NodeDefinition.PinType.DATA, NodeDefinition.PinDirection.INPUT, FlowType.ANY)); - outputs.add(new NodeDefinition.PinDefinition("out", NodeDefinition.PinType.FLOW, NodeDefinition.PinDirection.OUTPUT, FlowType.EXECUTION)); + visibleInputs.clear(); + inputs.add(new NodeDefinition.PinDefinition("in", NodeDefinition.PinType.DATA, NodeDefinition.PinDirection.INPUT, FlowDataType.ANY)); + outputs.add(new NodeDefinition.PinDefinition("out", NodeDefinition.PinType.FLOW, NodeDefinition.PinDirection.OUTPUT, FlowDataType.EXECUTION)); + visibleInputs.addAll(inputs); visibleOutputs.clear(); visibleOutputs.addAll(outputs); updateSize(); } - private int getPinColor(FlowType dataType) { + public static int getPinColor(FlowDataType dataType) { if (dataType == null) { return 0xFFAAAAAA; } @@ -481,8 +759,8 @@ protected void drawContent(IDrawContext ctx, int mouseX, int mouseY) { int rightColumnWidth = getRightColumnWidth(); int rightColumnStart = getX() + getWidth() - PADDING - rightColumnWidth; - for (int i = 0; i < inputs.size(); i++) { - NodeDefinition.PinDefinition input = inputs.get(i); + for (int i = 0; i < visibleInputs.size(); i++) { + NodeDefinition.PinDefinition input = visibleInputs.get(i); int rowY = getRowStartY() + i * (ROW_HEIGHT + ROW_SPACING); int pinY = rowY + (ROW_HEIGHT - PIN_BUTTON_SIZE) / 2; int pinX = getX() + PADDING; @@ -506,8 +784,8 @@ protected void drawContent(IDrawContext ctx, int mouseX, int mouseY) { drawPinButton(ctx, pinX, pinY, getPinColor(output.getDataType())); } - for (int i = inputs.size() - 1; i >= 0; i--) { - NodeDefinition.PinDefinition input = inputs.get(i); + for (int i = visibleInputs.size() - 1; i >= 0; i--) { + NodeDefinition.PinDefinition input = visibleInputs.get(i); Widget inputWidget = inputWidgets.get(input.getName()); if (inputWidget != null) { inputWidget.render(ctx, mouseX, mouseY, 0); @@ -528,7 +806,7 @@ protected void drawContent(IDrawContext ctx, int mouseX, int mouseY) { } private void updateSize() { - int rowCount = Math.max(inputs.size(), visibleOutputs.size()); + int rowCount = Math.max(visibleInputs.size(), visibleOutputs.size()); int leftColumnWidth = getLeftColumnWidth(); int rightColumnWidth = getRightColumnWidth(); int contentWidth = leftColumnWidth + rightColumnWidth + (leftColumnWidth > 0 && rightColumnWidth > 0 ? COLUMN_GAP : 0); @@ -536,7 +814,7 @@ private void updateSize() { if (addBranchButton != null && addBranchButton.visible) { contentHeight += ROW_HEIGHT + ROW_SPACING; } - int minWidth = (inputs.isEmpty() || visibleOutputs.isEmpty()) ? SINGLE_COLUMN_MIN_WIDTH : DEFAULT_WIDTH; + int minWidth = (visibleInputs.isEmpty() || visibleOutputs.isEmpty()) ? SINGLE_COLUMN_MIN_WIDTH : DEFAULT_WIDTH; int titleWidth = tr.getWidth(definition != null ? definition.getDisplayName() : node.getType()) + PADDING * 2; if (closeButton.visible) { titleWidth += CLOSE_BUTTON_WIDTH + PADDING; @@ -550,7 +828,7 @@ private void updateSize() { } public double[] getPinBounds(String pinName, boolean isInput) { - List pins = isInput ? inputs : visibleOutputs; + List pins = isInput ? visibleInputs : visibleOutputs; int index = -1; for (int i = 0; i < pins.size(); i++) { @@ -588,7 +866,7 @@ public Widget getOutputWidgetAt(int wx, int wy) { public Widget getInputWidgetAt(int wx, int wy) { updateInputWidgetPositions(); for (Widget widget : inputWidgets.values()) { - if (widget.isMouseOver(wx, wy)) { + if (widget.isVisible() && widget.isMouseOver(wx, wy)) { return widget; } } @@ -723,8 +1001,8 @@ private void updateInputWidgetPositions() { int leftColumnWidth = getLeftColumnWidth(); int leftColumnEnd = getX() + PADDING + leftColumnWidth; - for (int i = 0; i < inputs.size(); i++) { - NodeDefinition.PinDefinition input = inputs.get(i); + for (int i = 0; i < visibleInputs.size(); i++) { + NodeDefinition.PinDefinition input = visibleInputs.get(i); Widget inputWidget = inputWidgets.get(input.getName()); if (inputWidget != null) { int rowY = getRowStartY() + i * (ROW_HEIGHT + ROW_SPACING); @@ -735,12 +1013,18 @@ private void updateInputWidgetPositions() { inputWidget.setPosition(widgetX, widgetY); inputWidget.setWidth(widgetWidth); inputWidget.setHeight(widgetHeight); - inputWidget.setPriority(inputs.size() - i); + inputWidget.setPriority(visibleInputs.size() - i); if (inputWidget instanceof AnimatedWidget w) { - w.setLayer(inputs.size() - i); + w.setLayer(visibleInputs.size() - i); } } } + for (NodeDefinition.PinDefinition input : inputs) { + Widget inputWidget = inputWidgets.get(input.getName()); + if (inputWidget != null && !visibleInputs.contains(input)) { + inputWidget.setVisible(false); + } + } } @Override @@ -758,7 +1042,7 @@ protected void drawBackground(IDrawContext ctx) { private int getLeftColumnWidth() { int width = 0; - for (NodeDefinition.PinDefinition input : inputs) { + for (NodeDefinition.PinDefinition input : visibleInputs) { int labelWidth = tr.getWidth(input.getName()); int rowWidth = PIN_BUTTON_SIZE + PIN_TEXT_GAP + labelWidth; Widget widget = inputWidgets.get(input.getName()); @@ -812,6 +1096,9 @@ private void saveInputValue() { continue; } Widget widget = entry.getValue(); + if (!widget.isVisible()) { + continue; + } Object typedValue = null; if (widget instanceof TextInputWidget textInput) { String value = textInput.getText(); @@ -829,6 +1116,22 @@ private void saveInputValue() { continue; } typedValue = convertValue(selected.toString(), def.getDataType()); + } else if (widget instanceof SliderWidget slider) { + typedValue = slider.getValue(); + } else if (widget instanceof TextAreaWidget textArea) { + String value = textArea.getText(); + if (value.isEmpty()) { + node.getInputValues().remove(entry.getKey()); + continue; + } + typedValue = value; + } else if (widget instanceof ColorFieldWidget colorField) { + String value = colorField.getColor(); + if (value.isEmpty()) { + node.getInputValues().remove(entry.getKey()); + continue; + } + typedValue = value; } if (typedValue != null) { node.getInputValues().put(entry.getKey(), typedValue); @@ -840,14 +1143,14 @@ private int getInputWidgetWidth(Widget widget) { if (widget instanceof ToggleWidget) { return TOGGLE_WIDGET_WIDTH; } - return INPUT_WIDGET_WIDTH; + return widget.getWidth(); } private int getInputWidgetHeight(Widget widget) { if (widget instanceof ToggleWidget) { return TOGGLE_WIDGET_HEIGHT; } - return INPUT_WIDGET_HEIGHT; + return widget.getHeight(); } private NodeDefinition.PinDefinition findInputDefinition(String pinName) { @@ -859,23 +1162,25 @@ private NodeDefinition.PinDefinition findInputDefinition(String pinName) { return null; } - private Object convertValue(String value, FlowType dataType) { - if (dataType == null || dataType == FlowType.ANY) { + private Object convertValue(String value, FlowDataType dataType) { + if (dataType == null || dataType == FlowDataType.ANY) { return value; } + String id = dataType.getId(); try { - return switch (dataType) { - case STRING, EXECUTION, PLAYER, LOCATION, ITEM, LIST, ENTITY, ITEMSTACK, JSON_OBJECT -> value; - case NUMBER -> Double.parseDouble(value); - case BOOLEAN -> Boolean.parseBoolean(value); - case ANY -> value; - }; + if ("number".equals(id)) { + return Double.parseDouble(value); + } + if ("boolean".equals(id)) { + return Boolean.parseBoolean(value); + } + return value; } catch (NumberFormatException e) { return value; } } - public FlowType getPinType(String pinName, boolean isInput) { + public FlowDataType getPinType(String pinName, boolean isInput) { if (isInput) { for (NodeDefinition.PinDefinition input : inputs) { if (input.getName().equals(pinName)) { @@ -918,7 +1223,7 @@ public NodeDefinition.PinType getPinKind(String pinName, boolean isInput) { } public String getPinAtPosition(int wx, int wy) { - for (NodeDefinition.PinDefinition input : inputs) { + for (NodeDefinition.PinDefinition input : visibleInputs) { double[] bounds = getPinBounds(input.getName(), true); if (bounds != null && isInside(wx, wy, bounds)) { return input.getName(); @@ -937,9 +1242,16 @@ private void createOutputWidgets() { visibleOutputs.clear(); flowBranches.clear(); + List metadataVisibleOutputs = new ArrayList<>(); + for (NodeDefinition.PinDefinition output : outputs) { + if (evaluateVisibleWhen(output.getVisibleWhen())) { + metadataVisibleOutputs.add(output); + } + } + List flowOutputs = new ArrayList<>(); List otherOutputs = new ArrayList<>(); - for (NodeDefinition.PinDefinition output : outputs) { + for (NodeDefinition.PinDefinition output : metadataVisibleOutputs) { if (isFlowOutput(output)) { flowOutputs.add(output); } else { @@ -948,7 +1260,9 @@ private void createOutputWidgets() { } if (flowOutputs.size() <= 2) { - visibleOutputs.addAll(outputs); + visibleOutputs.addAll(flowOutputs); + visibleOutputs.addAll(otherOutputs); + visibleOutputs.sort((left, right) -> Boolean.compare(!isFlowOutput(left), !isFlowOutput(right))); addBranchButton = null; return; } @@ -956,19 +1270,20 @@ private void createOutputWidgets() { List selectedBranches = resolveFlowBranches(flowOutputs); for (String branch : selectedBranches) { NodeDefinition.PinDefinition pin = findOutputDefinition(branch); - if (pin != null) { + if (pin != null && metadataVisibleOutputs.contains(pin)) { visibleOutputs.add(pin); flowBranches.add(new FlowBranch(branch, buildBranchSelector(flowOutputs, branch))); } } visibleOutputs.addAll(otherOutputs); + visibleOutputs.sort((left, right) -> Boolean.compare(!isFlowOutput(left), !isFlowOutput(right))); saveFlowBranches(); updateAddBranchButton(flowOutputs); } private boolean isFlowOutput(NodeDefinition.PinDefinition output) { - return output.getType() == NodeDefinition.PinType.FLOW && output.getDataType() == FlowType.EXECUTION; + return output.getType() == NodeDefinition.PinType.FLOW && output.getDataType() == FlowDataType.EXECUTION; } private NodeDefinition.PinDefinition findOutputDefinition(String name) { @@ -1144,7 +1459,7 @@ private void updateOutputWidgetPositions() { } if (addBranchButton != null && addBranchButton.visible) { - int rowCount = Math.max(inputs.size(), visibleOutputs.size()); + int rowCount = Math.max(visibleInputs.size(), visibleOutputs.size()); int rowY = getRowStartY() + rowCount * (ROW_HEIGHT + ROW_SPACING); addBranchButton.setPosition(buttonX, rowY); addBranchButton.setWidth(Math.min(OUTPUT_WIDGET_WIDTH, rightColumnWidth));