Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified RemotelyMod/libs/ReScreen-1.0.jar
Binary file not shown.
Binary file modified RemotelyMod/libs/Rebase-1.0-SNAPSHOT.jar
Binary file not shown.
Binary file modified RemotelyMod/libs/Remotely-App.jar
Binary file not shown.
Binary file modified libs/ReScreen-1.0.jar
Binary file not shown.
Binary file modified libs/Rebase-1.0-SNAPSHOT.jar
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<NodeDefinition.NodeCategory>() {
@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 {

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The custom Gson adapter for NodeDefinition.NodeCategory calls in.nextString() unconditionally. If the server sends null for category (or the field is missing and Gson presents a NULL token), this will throw and abort snapshot parsing. Handle JsonToken.NULL similarly to the FlowDataType adapter (consume null and return a sensible default like NodeCategory.UTILITY).

Suggested change
public NodeDefinition.NodeCategory read(com.google.gson.stream.JsonReader in) throws IOException {
public NodeDefinition.NodeCategory read(com.google.gson.stream.JsonReader in) throws IOException {
if (in.peek() == com.google.gson.stream.JsonToken.NULL) {
in.nextNull();
return NodeDefinition.NodeCategory.UTILITY;
}

Copilot uses AI. Check for mistakes.
String id = in.nextString();
return NodeDefinition.NodeCategory.fromString(id);
}
Comment on lines +85 to +88

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NodeCategory.fromString(id) falls back to UTILITY for unknown IDs. If the server snapshot contains categories not pre-registered on the client, those node definitions will be deserialized into the wrong category and the original ID is lost. Consider preserving unknown IDs (e.g., register a placeholder category on-the-fly during deserialization, or represent categories as string IDs in DTOs and resolve them after category metadata is applied).

Copilot uses AI. Check for mistakes.
})
.create();
private final Queue<Runnable> pendingSends = new ConcurrentLinkedQueue<>();
private final Map<ReSyncResourceType, Set<String>> pendingOpenResources = new ConcurrentHashMap<>();
private final NodeRegistryCache nodeRegistryCache = NodeRegistryCache.getInstance();
Expand Down
148 changes: 148 additions & 0 deletions src/main/java/redxax/oxy/remotely/flow/data/FlowDataType.java
Original file line number Diff line number Diff line change
@@ -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<String, FlowDataType> 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);
}
Comment on lines +58 to +65

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FlowDataType stores registry entries under the raw id (REGISTRY.put(id, this)), but lookups use id.toLowerCase() in fromString()/registerServerType(). If the server sends a type id with any uppercase/mixed-case, it will be inserted under a different key and will never be found, causing deserialization to fall back to ANY unexpectedly. Normalize keys on insert (e.g., store under id.toLowerCase(Locale.ROOT) and consider normalizing the id field itself) so registry behavior is consistent.

Copilot uses AI. Check for mistakes.

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<FlowDataType> 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) {
Comment on lines +105 to +115

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REGISTRY is a plain LinkedHashMap, but types are registered from NodeRegistry.applySnapshot(...) which is triggered from the websocket thread while UI code calls values()/fromString() concurrently. This can lead to race conditions / ConcurrentModificationException. Consider making the registry thread-safe (e.g., ConcurrentHashMap + deterministic ordering, or synchronizing all registry reads/writes and returning defensive copies from values()).

Suggested change
public static List<FlowDataType> 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) {
public static synchronized List<FlowDataType> values() {
return REGISTRY.values().stream().distinct().filter(type -> type != EXECUTION).toList();
}
public static synchronized FlowDataType fromString(String id) {
if (id == null || id.isEmpty()) return ANY;
FlowDataType type = REGISTRY.get(id.toLowerCase());
return type != null ? type : ANY;
}
public static synchronized FlowDataType registerServerType(String id, String displayName, int color, String parentId, boolean canStringify) {

Copilot uses AI. Check for mistakes.
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();
}
}
Original file line number Diff line number Diff line change
@@ -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<FlowDataType> {
@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);
}
Comment on lines +22 to +23

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FlowDataTypeAdapter.read() resolves IDs via FlowDataType.fromString(id), which returns ANY for unknown IDs. During node-registry snapshot parsing, server-defined/custom types will be deserialized as ANY before NodeRegistry.applySnapshot() registers them, losing the original type information. Consider preserving unknown IDs by creating/registering a placeholder FlowDataType during deserialization (or deferring resolution until after metadata registration).

Suggested change
return FlowDataType.fromString(id);
}
return resolveFlowDataType(id);
}
private FlowDataType resolveFlowDataType(String id) throws IOException {
FlowDataType resolved = FlowDataType.fromString(id);
if (resolved == null) {
FlowDataType placeholder = createPlaceholderType(id);
if (placeholder != null) {
return placeholder;
}
throw new IOException("Unable to deserialize FlowDataType with id '" + id + "'");
}
String resolvedId = resolved.getId();
if (resolvedId != null && resolvedId.equals(id)) {
return resolved;
}
FlowDataType placeholder = createPlaceholderType(id);
if (placeholder != null) {
return placeholder;
}
throw new IOException(
"Unknown FlowDataType id '" + id + "' would be deserialized as '" + resolvedId
+ "', losing the original type information"
);
}
private FlowDataType createPlaceholderType(String id) {
FlowDataType created = invokeStaticFactory("register", id);
if (created != null) {
return created;
}
created = invokeStaticFactory("of", id);
if (created != null) {
return created;
}
created = invokeStaticFactory("create", id);
if (created != null) {
return created;
}
created = invokeStaticFactory("valueOf", id);
if (created != null) {
return created;
}
try {
java.lang.reflect.Constructor<FlowDataType> constructor = FlowDataType.class.getDeclaredConstructor(String.class);
constructor.setAccessible(true);
return constructor.newInstance(id);
} catch (ReflectiveOperationException ignored) {
// Fall through to other supported construction shapes.
}
try {
java.lang.reflect.Constructor<FlowDataType> constructor = FlowDataType.class.getDeclaredConstructor(String.class, String.class);
constructor.setAccessible(true);
return constructor.newInstance(id, id);
} catch (ReflectiveOperationException ignored) {
return null;
}
}
private FlowDataType invokeStaticFactory(String methodName, String id) {
try {
java.lang.reflect.Method method = FlowDataType.class.getDeclaredMethod(methodName, String.class);
method.setAccessible(true);
Object value = method.invoke(null, id);
if (value instanceof FlowDataType) {
return (FlowDataType) value;
}
} catch (ReflectiveOperationException ignored) {
// Try the next available factory/constructor option.
}
return null;
}

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +23

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FlowDataTypeAdapter.read() calls in.nextString() unconditionally. If the JSON contains null (or the field is missing and Gson feeds a NULL token), this will throw and abort deserialization. Handle JsonToken.NULL by consuming it and returning null or FlowDataType.ANY (whichever the codebase expects).

Copilot uses AI. Check for mistakes.
}
12 changes: 6 additions & 6 deletions src/main/java/redxax/oxy/remotely/flow/data/FlowGraph.java
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
66 changes: 0 additions & 66 deletions src/main/java/redxax/oxy/remotely/flow/data/FlowType.java

This file was deleted.

Loading
Loading