list) {
if (list == null) {
diff --git a/core/src/main/java/dev/kejona/crossplatforms/serialize/KeyedType.java b/core/src/main/java/dev/kejona/crossplatforms/serialize/KeyedType.java
index d5ae7390..42ce7cad 100644
--- a/core/src/main/java/dev/kejona/crossplatforms/serialize/KeyedType.java
+++ b/core/src/main/java/dev/kejona/crossplatforms/serialize/KeyedType.java
@@ -1,22 +1,26 @@
package dev.kejona.crossplatforms.serialize;
+import org.spongepowered.configurate.ConfigurationNode;
+
/**
- * For use with {@link KeyedTypeSerializer}
+ * For use with {@link KeyedTypeSerializer}. Implementing classes must adhere to {@link ConfigurationNode#isMap()}
+ * in order to be successfully serialized. They do not need to be concerned with containing a type field,
+ * as {@link KeyedTypeSerializer} handles the reading and writing of the type.
+ *
+ * However, the implementation of {@link KeyedType#type()} must be exactly equal to the String type identifier
+ * provided in {@link KeyedTypeSerializer#registerType(String, Class)} when this implementation is registered.
*/
public interface KeyedType {
- /**
- * @return The identifier for the type of this implementation
- */
String type();
/**
- * @return An object serializable by Configurate that represents this implementation, which should be successfully
- * deserialized back to an equivalent instance of this implementation if necessary.
- *
- * The default implementation is that this returns the given instance that this is called on.
+ * @return false if the type of a serialized form can always be inferred, meaning that the type does not
+ * have to be included in the serialization. If it can sometimes or never be inferred, then true. {@link TypeResolver}s
+ * that are registered to a {@link KeyedTypeSerializer} are responsible for inferring the type to deserialize as.
+ * @see KeyedTypeSerializer#registerType(String, Class, TypeResolver)
*/
- default Object value() {
- return this;
+ default boolean serializeWithType() {
+ return true;
}
}
diff --git a/core/src/main/java/dev/kejona/crossplatforms/serialize/KeyedTypeListSerializer.java b/core/src/main/java/dev/kejona/crossplatforms/serialize/KeyedTypeListSerializer.java
deleted file mode 100644
index 83e9a713..00000000
--- a/core/src/main/java/dev/kejona/crossplatforms/serialize/KeyedTypeListSerializer.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package dev.kejona.crossplatforms.serialize;
-
-import org.checkerframework.checker.nullness.qual.Nullable;
-import org.spongepowered.configurate.ConfigurationNode;
-import org.spongepowered.configurate.serialize.SerializationException;
-import org.spongepowered.configurate.serialize.TypeSerializer;
-
-import java.lang.reflect.ParameterizedType;
-import java.lang.reflect.Type;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Deserializes a map with keys of type identifiers (strings) and values that are instances of type provided type T,
- * into a list of T.
- * @param The parent type that all entry values have in common.
- */
-public class KeyedTypeListSerializer implements TypeSerializer> {
-
- private final KeyedTypeSerializer elementSerializer;
-
- public KeyedTypeListSerializer(KeyedTypeSerializer elementSerializer) {
- this.elementSerializer = elementSerializer;
- }
-
- @Override
- public List deserialize(Type type, ConfigurationNode node) throws SerializationException {
- if (!(type instanceof ParameterizedType)) {
- throw new SerializationException("Cannot deserialize to list with no element type parameter");
- }
-
- ParameterizedType parameterized = (ParameterizedType) type;
- Type[] typeArgs = parameterized.getActualTypeArguments();
- if (typeArgs.length < 1) {
- throw new SerializationException("Cannot deserialize to a list that is a raw type");
- }
- Type elementType = parameterized.getActualTypeArguments()[0];
-
- List mapped = new ArrayList<>();
- for (ConfigurationNode child : node.childrenMap().values()) {
- mapped.add(elementSerializer.deserialize(elementType, child));
- }
-
- return mapped;
- }
-
- @Override
- public void serialize(Type type, @Nullable List list, ConfigurationNode node) throws SerializationException {
- node.raw(null);
-
- if (list != null) {
- node.set(Collections.emptyMap());
-
- for (E element : list) {
- // KeyedType decides what value should be serialized. If it is a non-simple type, it is expected that
- // the class instance is passed. If its a simple type, its expected that the singleton value is passed.
- node.node(element.type()).set(element.value());
- }
- }
- }
-}
diff --git a/core/src/main/java/dev/kejona/crossplatforms/serialize/KeyedTypeSerializer.java b/core/src/main/java/dev/kejona/crossplatforms/serialize/KeyedTypeSerializer.java
index 9a9c5319..60d741e8 100644
--- a/core/src/main/java/dev/kejona/crossplatforms/serialize/KeyedTypeSerializer.java
+++ b/core/src/main/java/dev/kejona/crossplatforms/serialize/KeyedTypeSerializer.java
@@ -1,112 +1,99 @@
package dev.kejona.crossplatforms.serialize;
-import com.google.inject.AbstractModule;
-import com.google.inject.Injector;
-import dev.kejona.crossplatforms.Entry;
-import dev.kejona.crossplatforms.utils.ConfigurateUtils;
-import io.leangen.geantyref.TypeToken;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.serialize.SerializationException;
import org.spongepowered.configurate.serialize.TypeSerializer;
import org.spongepowered.configurate.serialize.TypeSerializerCollection;
-import javax.annotation.Nonnull;
import java.lang.reflect.Type;
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
/**
- * Deserializes a value node into {@link T} depending on the key of the node. This serializer must be registered using
- * {@link TypeSerializerCollection.Builder#registerExact(Class, TypeSerializer)} or
- * {@link TypeSerializerCollection.Builder#registerExact(TypeToken, TypeSerializer)} or
- * @param A parent type that all possible deserializations of the node share
+ * Deserializes a value {@link T} depending on a string node within the node representing {@link T}.
+ * This serializer must be registered with one of the registerExact methods of {@link TypeSerializerCollection.Builder}
+ * @param The parent type that all entry values have in common.
*/
public class KeyedTypeSerializer extends TypeRegistry implements TypeSerializer {
- private final Injector injector;
- private final Map, Class extends T>>> simpleTypes = new HashMap<>();
+ private final String typeKey;
+ private final List typeResolvers = new ArrayList<>();
- public KeyedTypeSerializer(Injector injector) {
- this.injector = injector;
+ /**
+ * Creates a ValuedTypeSerializer with the given key to read the type at
+ * @param typeKey The key that the type value is expected to reside at when deserializing/serializing
+ */
+ public KeyedTypeSerializer(String typeKey) {
+ this.typeKey = Objects.requireNonNull(typeKey);
}
- @Nonnull
- @Override
- public Set getTypes(Type superType) {
- Set types = filter(
- superType,
- simpleTypes.entrySet().stream().map(e -> Entry.of(e.getKey(), e.getValue().getValue()))
- );
-
- types.addAll(super.getTypes(superType));
- return types;
+ /**
+ * Creates a ValuedTypeSerializer with a type key of "type".
+ */
+ public KeyedTypeSerializer() {
+ this("type");
}
- public void registerSimpleType(String typeId, TypeToken valueType, Class extends T> simpleType) {
- String lowerCase = typeId.toLowerCase(Locale.ROOT);
- if (simpleTypes.get(lowerCase) != null) {
- throw new IllegalArgumentException("Simple Type " + lowerCase + " is already registered");
- }
- simpleTypes.put(lowerCase, Entry.of(valueType, simpleType));
- }
-
- public void registerSimpleType(String typeId, Class valueType, Class extends T> simpleType) {
- registerSimpleType(typeId, TypeToken.get(valueType), simpleType);
+ public void registerType(String typeId, Class extends T> type, TypeResolver typeResolver) {
+ registerType(typeId, type);
+ typeResolvers.add(typeResolver);
}
@Override
public T deserialize(Type returnType, ConfigurationNode node) throws SerializationException {
- Object key = node.key();
- if (key == null || key.toString().equals("")) {
- throw new SerializationException("Cannot deserialization a node into a KeyedType with a key of: " + key);
+ String typeId = node.node(typeKey).getString();
+ if (typeId == null) {
+ // try to infer the type based off the nodes content
+ for (TypeResolver resolver : typeResolvers) {
+ String possibleType = resolver.inferType(node);
+ if (possibleType != null) {
+ if (typeId != null) {
+ throw new SerializationException("Failed to infer the type because both types matched: " + typeId + " and " + possibleType);
+ }
+ typeId = possibleType;
+ }
+ }
+
+ if (typeId == null) {
+ throw new SerializationException("No 'type' value present and the type could not be inferred. Possible type options are: " + getTypes(returnType));
+ }
}
- String typeId = key.toString();
- Class extends T> type = getType(typeId);
- T instance;
+ Class extends T> type = getType(typeId);
if (type == null) {
- Entry, Class extends T>> simpleType = simpleTypes.get(typeId.toLowerCase(Locale.ROOT));
- if (simpleType == null) {
- throw new SerializationException("Unsupported type (not registered) '" + typeId + "'. Possible options are: " + getTypes(returnType));
- } else {
- validateType(returnType, typeId, simpleType.getValue());
- instance = deserializeSimple(simpleType.getKey(), simpleType.getValue(), node);
- }
- } else {
- validateType(returnType, typeId, type);
- instance = node.get(type);
+ throw new SerializationException("Unsupported type '" + typeId + "'. Not registered. Possible options are: " + getTypes(returnType));
}
- if (instance == null) {
- throw new SerializationException("Failed to deserialize as '" + typeId + "' because deserialization returned null.");
+
+ if (!isCompatible(returnType, type)) {
+ throw new SerializationException("Unsupported type '" + typeId + "'. " + type + " is registered but incompatible. Possible type options are: " + getTypes(returnType));
}
- return instance;
+
+ T object = node.get(type);
+ if (object == null) {
+ throw new SerializationException("Failed to deserialize as '" + type + "' because deserialization returned null.");
+ }
+
+ return object;
}
@Override
- public void serialize(Type type, @Nullable T keyedType, ConfigurationNode node) throws SerializationException {
+ public void serialize(Type returnType, @Nullable T value, ConfigurationNode node) throws SerializationException {
node.raw(null);
- if (keyedType != null) {
- if (keyedType.type().equals(node.key())) {
- node.set(keyedType.value());
- } else {
- throw new SerializationException("Cannot deserialize '" + keyedType.value() + "' of type '" + keyedType.type() + "' because the key of the node at " + node.path() + " does not match the type");
+ if (value != null) {
+ String typeIdentifier = value.type();
+ if (getType(typeIdentifier) == null) {
+ // we don't actually need it for serializing but its probably a mistake or bad design
+ throw new SerializationException("Cannot serialize implementation of ValueType " + value.getClass() + " that has not been registered");
}
- }
- }
- private T deserializeSimple(TypeToken valueType, Class extends T> simpleType, ConfigurationNode node) throws SerializationException {
- final V value = node.get(valueType);
- Injector childInjector = injector.createChildInjector(new AbstractModule() {
- @Override
- protected void configure() {
- bind(ConfigurateUtils.keyFromToken(valueType)).toInstance(value);
+ node.set(value);
+ if (value.serializeWithType()) {
+ // the type must be included because when this node is deserialized, the type cannot be inferred
+ node.node(typeKey).set(typeIdentifier);
}
- });
-
- return childInjector.getInstance(simpleType);
+ }
}
}
diff --git a/core/src/main/java/dev/kejona/crossplatforms/serialize/SimpleType.java b/core/src/main/java/dev/kejona/crossplatforms/serialize/SimpleType.java
deleted file mode 100644
index b816d100..00000000
--- a/core/src/main/java/dev/kejona/crossplatforms/serialize/SimpleType.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package dev.kejona.crossplatforms.serialize;
-
-import lombok.AllArgsConstructor;
-import lombok.ToString;
-
-import javax.annotation.Nonnull;
-
-@ToString
-@AllArgsConstructor
-public abstract class SimpleType implements KeyedType {
-
- @Nonnull
- private final String type;
-
- @Nonnull
- private final V value;
-
- @Override
- public final String type() {
- return type;
- }
-
- @Override
- public final V value() {
- return value;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- SimpleType> that = (SimpleType>) o;
- return type.equals(that.type) && value.equals(that.value);
- }
-}
diff --git a/core/src/main/java/dev/kejona/crossplatforms/serialize/TypeRegistry.java b/core/src/main/java/dev/kejona/crossplatforms/serialize/TypeRegistry.java
index 35b0349f..808d424b 100644
--- a/core/src/main/java/dev/kejona/crossplatforms/serialize/TypeRegistry.java
+++ b/core/src/main/java/dev/kejona/crossplatforms/serialize/TypeRegistry.java
@@ -1,6 +1,6 @@
package dev.kejona.crossplatforms.serialize;
-import org.spongepowered.configurate.serialize.SerializationException;
+import io.leangen.geantyref.GenericTypeReflector;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -10,9 +10,6 @@
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static io.leangen.geantyref.GenericTypeReflector.isSuperType;
/**
* Deserializes a map with keys of type identifiers (strings) and values that are instances of type provided type T,
@@ -32,7 +29,7 @@ public class TypeRegistry {
*/
public void registerType(String typeId, Class extends T> type) {
String lowerCase = typeId.toLowerCase(Locale.ROOT);
- if (types.get(lowerCase) != null) {
+ if (types.containsKey(lowerCase)) {
throw new IllegalArgumentException("Type " + lowerCase + " is already registered");
}
types.put(lowerCase, type);
@@ -50,26 +47,19 @@ public Class extends T> getType(String typeId) {
@Nonnull
public Set getTypes(Type superType) {
- return filter(superType, types);
- }
-
- public void validateType(Type required, String typeKey, Type registered) throws SerializationException {
- if (!isValidType(required, registered)) {
- throw new SerializationException("Unsupported type '" + typeKey + "'. " + registered + " is registered but incompatible. Possible type options are: " + getTypes(required));
- }
- }
-
- public boolean isValidType(Type required, Type registered) {
- return isSuperType(required, registered);
- }
-
- public final Set filter(Type superType, Map> types) {
- return filter(superType, types.entrySet().stream());
- }
-
- public final Set filter(Type superType, Stream>> types) {
- return types.filter(e -> isValidType(superType, e.getValue()))
+ return types.entrySet().stream()
+ .filter(e -> isCompatible(superType, e.getValue()))
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
}
+
+ /**
+ * Tests if a registered Type is compatible with some required Type.
+ * @param required the Type that is required. For example, in the context of serialization, this would be the declared field type.
+ * @param registered a Type that is registered in a Registry
+ * @return true if the registered type is a subtype of the required type
+ */
+ public static boolean isCompatible(Type required, Type registered) {
+ return GenericTypeReflector.isSuperType(required, registered);
+ }
}
diff --git a/core/src/main/java/dev/kejona/crossplatforms/serialize/TypeResolver.java b/core/src/main/java/dev/kejona/crossplatforms/serialize/TypeResolver.java
index 7280c31b..df3e7dcf 100644
--- a/core/src/main/java/dev/kejona/crossplatforms/serialize/TypeResolver.java
+++ b/core/src/main/java/dev/kejona/crossplatforms/serialize/TypeResolver.java
@@ -9,7 +9,7 @@
public interface TypeResolver {
@Nullable
- String getType(ConfigurationNode node);
+ String inferType(ConfigurationNode node);
static TypeResolver listOrScalar(String childKey, String type) {
return node -> {
diff --git a/core/src/main/java/dev/kejona/crossplatforms/serialize/ValuedType.java b/core/src/main/java/dev/kejona/crossplatforms/serialize/ValuedType.java
deleted file mode 100644
index 32031050..00000000
--- a/core/src/main/java/dev/kejona/crossplatforms/serialize/ValuedType.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package dev.kejona.crossplatforms.serialize;
-
-import org.spongepowered.configurate.ConfigurationNode;
-
-/**
- * For use with {@link ValuedTypeSerializer}. Implementing classes must adhere to {@link ConfigurationNode#isMap()}
- * in order to be successfully serialized. They, do not need to be concerned with containing a type field,
- * as {@link ValuedTypeSerializer} handles the reading and writing of the type.
- *
- * However, the implementation of {@link ValuedType#type()} must be exactly equal to the String type identifier
- * provided in {@link ValuedTypeSerializer#registerType(String, Class)} when this implementation is registered.
- */
-public interface ValuedType {
-
- String type();
-
- default boolean serializeWithType() {
- return true;
- }
-}
diff --git a/core/src/main/java/dev/kejona/crossplatforms/serialize/ValuedTypeSerializer.java b/core/src/main/java/dev/kejona/crossplatforms/serialize/ValuedTypeSerializer.java
deleted file mode 100644
index 7796f3af..00000000
--- a/core/src/main/java/dev/kejona/crossplatforms/serialize/ValuedTypeSerializer.java
+++ /dev/null
@@ -1,99 +0,0 @@
-package dev.kejona.crossplatforms.serialize;
-
-import lombok.Getter;
-import org.checkerframework.checker.nullness.qual.Nullable;
-import org.spongepowered.configurate.ConfigurationNode;
-import org.spongepowered.configurate.serialize.SerializationException;
-import org.spongepowered.configurate.serialize.TypeSerializer;
-
-import java.lang.reflect.Type;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-
-/**
- * Deserializes a value {@link T} depending on a string node in the node representing {@link T}.
- * This serializer must be registered exact.
- * @param The parent type that all entry values have in common.
- */
-public class ValuedTypeSerializer extends TypeRegistry implements TypeSerializer {
-
- @Getter
- private final String typeKey;
- private final List typeResolvers = new ArrayList<>();
-
- /**
- * Creates a ValuedTypeSerializer with the given key to read the type at
- * @param typeKey The key that the type value is expected to reside at when deserializing/serializing
- */
- public ValuedTypeSerializer(String typeKey) {
- Objects.requireNonNull(typeKey);
- this.typeKey = typeKey;
- }
-
- /**
- * Creates a ValuedTypeSerializer with a type key of "type".
- */
- public ValuedTypeSerializer() {
- this("type");
- }
-
- public void registerType(String typeId, Class extends T> type, TypeResolver typeResolver) {
- super.registerType(typeId, type);
- typeResolvers.add(typeResolver);
- }
-
- @Override
- public T deserialize(Type returnType, ConfigurationNode node) throws SerializationException {
- String typeId = node.node(typeKey).getString();
- if (typeId == null) {
- // try to infer the type based off the nodes content
- for (TypeResolver resolver : typeResolvers) {
- String possibleType = resolver.getType(node);
- if (possibleType != null) {
- if (typeId != null) {
- throw new SerializationException("Failed to infer the type because both types matched: " + typeId + " and " + possibleType);
- }
- typeId = possibleType;
- }
- }
-
- if (typeId == null) {
- throw new SerializationException("No 'type' value present and the type could not be inferred. Possible type options are: " + getTypes(returnType));
- }
- }
-
- Class extends T> type = getType(typeId);
- if (type == null) {
- throw new SerializationException("Unsupported type '" + typeId + "'. Not registered. Possible options are: " + getTypes(returnType));
- }
-
- validateType(returnType, typeId, type);
-
- T object = node.get(type);
- if (object == null) {
- throw new SerializationException("Failed to deserialize as '" + type + "' because deserialization returned null.");
- }
-
- return object;
- }
-
- @Override
- public void serialize(Type returnType, @Nullable T value, ConfigurationNode node) throws SerializationException {
- node.raw(null);
-
- if (value != null) {
- String typeIdentifier = value.type();
- if (getType(typeIdentifier) == null) {
- // we don't actually need it for serializing but its probably a mistake or bad design
- throw new SerializationException("Cannot serialize implementation of ValueType " + value.getClass() + " that has not been registered");
- }
-
- node.set(value);
- if (value.serializeWithType()) {
- // the type must be included because when this node is deserialized, the type cannot be inferred
- node.node(typeKey).set(typeIdentifier);
- }
- }
- }
-}
diff --git a/core/src/main/java/dev/kejona/crossplatforms/utils/ParseUtils.java b/core/src/main/java/dev/kejona/crossplatforms/utils/ParseUtils.java
index e23f1853..7eb89476 100644
--- a/core/src/main/java/dev/kejona/crossplatforms/utils/ParseUtils.java
+++ b/core/src/main/java/dev/kejona/crossplatforms/utils/ParseUtils.java
@@ -41,6 +41,21 @@ public static int getUnsignedInt(@Nullable String value, String identifier) thro
}
}
+ /**
+ * Converts a float to an integer if it can be done without loss of precision.
+ * @param floaty The float, in string representation.
+ * @return the integer as a string if conversion was possible, otherwise the floaty parameter
+ * @throws NumberFormatException if the parameter could not be parsed to a float
+ */
+ public static String downSize(String floaty) throws NumberFormatException {
+ float value = Float.parseFloat(floaty);
+ if ((int) value == value) {
+ return Integer.toString((int) value);
+ } else {
+ return floaty;
+ }
+ }
+
public static float getFloat(@Nullable String value, String identifier) throws IllegalValueException {
if (value == null) {
throw new IllegalValueException(null, "decimal number", identifier);
@@ -54,6 +69,17 @@ public static float getFloat(@Nullable String value, String identifier) throws I
}
}
+ /**
+ * Requires the string value to be a float, that is positive. It must not be zero or negative.
+ */
+ public static float getPositiveFloat(@Nullable String value, String identifier) throws IllegalValueException {
+ float f = getFloat(value, identifier);
+ if (f <= 0) {
+ throw new IllegalValueException(value, "positive decimal number", identifier);
+ }
+ return f;
+ }
+
public static boolean getBoolean(@Nullable String value, String identifier) throws IllegalValueException {
if (value == null) {
throw new IllegalValueException(null, "boolean", identifier);
diff --git a/core/src/main/java/dev/kejona/crossplatforms/utils/ReflectionUtils.java b/core/src/main/java/dev/kejona/crossplatforms/utils/ReflectionUtils.java
index ba64ac94..00f1294e 100644
--- a/core/src/main/java/dev/kejona/crossplatforms/utils/ReflectionUtils.java
+++ b/core/src/main/java/dev/kejona/crossplatforms/utils/ReflectionUtils.java
@@ -1,9 +1,12 @@
package dev.kejona.crossplatforms.utils;
+import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Objects;
public final class ReflectionUtils {
@@ -16,11 +19,15 @@ public static Class> getClass(String name) {
try {
return Class.forName(name);
} catch (ClassNotFoundException e) {
- e.printStackTrace();
return null;
}
}
+ @Nonnull
+ public static Class> requireClass(String name) {
+ return Objects.requireNonNull(getClass(name), "Class for name " + name);
+ }
+
@Nullable
public static Method getMethod(Class> clazz, String method, boolean declared, Class>... arguments) {
try {
@@ -42,13 +49,21 @@ public static Method getMethod(Class> clazz, String methodName, Class>... ar
return getMethod(clazz, methodName, false, arguments);
}
+ @Nonnull
+ public static Method requireMethod(Class> clazz, String methodName, Class>... arguments) {
+ return Objects.requireNonNull(
+ getMethod(clazz, methodName, arguments),
+ () -> methodName + " method of " + clazz.getName() + " with arguments " + Arrays.toString(arguments)
+ );
+ }
+
@Nullable
public static Object invoke(Object instance, Method method, Object... arguments) {
method.setAccessible(true);
try {
return method.invoke(instance, arguments);
- } catch (IllegalAccessException | InvocationTargetException exception) {
- exception.printStackTrace();
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ e.printStackTrace();
return null;
}
}
@@ -59,6 +74,34 @@ public static T castedInvoke(Object instance, Method method, Object... argum
return (T) invoke(instance, method, arguments);
}
+ @Nullable
+ public static Field getField(Class> clazz, String name, boolean declared) {
+ try {
+ if (declared) {
+ return clazz.getDeclaredField(name);
+ } else {
+ return clazz.getField(name);
+ }
+ } catch (NoSuchFieldException ignored) {
+ return null;
+ }
+ }
+
+ @Nullable
+ public static Field getField(Class> clazz, String name) {
+ Field field = getField(clazz, name, true); // try declared fields, private or public
+ if (field == null) {
+ field = getField(clazz, name, false); // try public inherited fields
+ }
+
+ return field;
+ }
+
+ @Nonnull
+ public static Field requireField(Class> clazz, String name) {
+ return Objects.requireNonNull(getField(clazz, name), () -> name + " field of " + clazz.getName());
+ }
+
@Nullable
public static Object getValue(Object instance, Field field) {
field.setAccessible(true);
@@ -68,4 +111,13 @@ public static Object getValue(Object instance, Field field) {
throw new IllegalStateException("Failed to set " + field.getName() + " on " + instance.getClass() + " accessible", e);
}
}
+
+ public static void setValue(Object instance, Field field, Object value) {
+ field.setAccessible(true);
+ try {
+ field.set(instance, value);
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
}
diff --git a/core/src/main/java/dev/kejona/crossplatforms/utils/SkinUtils.java b/core/src/main/java/dev/kejona/crossplatforms/utils/SkinUtils.java
new file mode 100644
index 00000000..787136c6
--- /dev/null
+++ b/core/src/main/java/dev/kejona/crossplatforms/utils/SkinUtils.java
@@ -0,0 +1,70 @@
+package dev.kejona.crossplatforms.utils;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+import javax.annotation.Nonnull;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public final class SkinUtils {
+
+ private static final Gson GSON = new Gson();
+ private static final Pattern URL_PATTERN = Pattern.compile("(http|https)://textures\\.minecraft\\.net/texture/([a-zA-Z0-9]+)");
+
+ private SkinUtils() {
+
+ }
+
+ @Nonnull
+ public static String urlFromEncoding(@Nonnull String encodedData) throws IllegalArgumentException {
+ // See https://wiki.vg/Mojang_API#UUID_to_Profile_and_Skin.2FCape
+ JsonObject json;
+ try {
+ String decoded = new String(Base64.getDecoder().decode(encodedData), StandardCharsets.UTF_8);
+ json = GSON.fromJson(decoded, JsonObject.class);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Failed to decode base64 textures value: " + encodedData, e);
+ }
+
+ JsonObject textures = json.getAsJsonObject("textures");
+ if (textures == null) {
+ throw new IllegalArgumentException("textures member missing in " + encodedData);
+ }
+
+ JsonObject skin = textures.getAsJsonObject("SKIN");
+ if (skin == null) {
+ throw new IllegalArgumentException("SKIN member missing in " + encodedData);
+ }
+
+ JsonElement url = skin.get("url");
+ if (url == null) {
+ throw new IllegalArgumentException("url member missing in " + encodedData);
+ }
+
+ try {
+ return Objects.requireNonNull(url.getAsString(), "url as string");
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Failed to get url element in " + encodedData, e);
+ }
+ }
+
+ @Nonnull
+ public static String idFromUrl(@Nonnull String skinUrl) throws IllegalArgumentException {
+ Matcher matcher = URL_PATTERN.matcher(skinUrl);
+ if (matcher.matches()) {
+ return matcher.group(2);
+ } else {
+ throw new IllegalArgumentException("Skin url has unexpected format: " + skinUrl);
+ }
+ }
+
+ @Nonnull
+ public static String idFromEncoding(@Nonnull String encodedData) throws IllegalArgumentException {
+ return idFromUrl(urlFromEncoding(encodedData));
+ }
+}
diff --git a/core/src/main/java16/dev/kejona/crossplatforms/handler/GeyserHandler.java b/core/src/main/java16/dev/kejona/crossplatforms/handler/GeyserHandler.java
index 47b5bb8f..21f89e5c 100644
--- a/core/src/main/java16/dev/kejona/crossplatforms/handler/GeyserHandler.java
+++ b/core/src/main/java16/dev/kejona/crossplatforms/handler/GeyserHandler.java
@@ -1,17 +1,15 @@
package dev.kejona.crossplatforms.handler;
-import org.geysermc.api.connection.Connection;
import org.geysermc.cumulus.form.Form;
-import org.geysermc.geyser.GeyserImpl;
-import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.api.GeyserApi;
+import org.geysermc.geyser.api.connection.GeyserConnection;
-import java.util.Objects;
import java.util.UUID;
@SuppressWarnings("unused") // loaded on JDK 16
public class GeyserHandler implements BedrockHandler {
- private final GeyserImpl geyser = Objects.requireNonNull(GeyserImpl.getInstance());
+ private final GeyserApi api = GeyserApi.api();
public static boolean supported() {
// method is used so that compiler doesn't inline value
@@ -25,17 +23,13 @@ public String getType() {
@Override
public boolean isBedrockPlayer(UUID uuid) {
- return geyser.connectionByUuid(uuid) != null;
+ return api.isBedrockPlayer(uuid);
}
@Override
public void sendForm(UUID uuid, Form form) {
- GeyserSession session = geyser.connectionByUuid(uuid);
- if (session == null) {
- throw new NullPointerException("Failed to get GeyserSession for UUID " + uuid);
- } else {
- session.getFormCache().showForm(form);
- }
+ // don't use GeyserApi#sendForm since it only returns false when there is no session found for the uuid
+ requireConnection(uuid).sendForm(form);
}
@Override
@@ -45,11 +39,16 @@ public boolean executesResponseHandlersSafely() {
@Override
public boolean transfer(FormPlayer player, String address, int port) {
- Connection connection = geyser.connectionByUuid(player.getUuid());
+ // don't use GeyserApi#transfer since it only returns false when there is no session found for the uuid
+ return requireConnection(player.getUuid()).transfer(address, port);
+ }
+
+ private GeyserConnection requireConnection(UUID uuid) {
+ GeyserConnection connection = api.connectionByUuid(uuid);
if (connection == null) {
- throw new IllegalArgumentException("Failed to find GeyserSession for " + player.getName() + player.getUuid());
- } else {
- return connection.transfer(address, port);
+ throw new NullPointerException("Failed to get GeyserConnection for UUID: " + uuid);
}
+
+ return connection;
}
}
diff --git a/core/src/test/java/dev/kejona/crossplatforms/ParseUtilsTest.java b/core/src/test/java/dev/kejona/crossplatforms/ParseUtilsTest.java
new file mode 100644
index 00000000..5991ee0d
--- /dev/null
+++ b/core/src/test/java/dev/kejona/crossplatforms/ParseUtilsTest.java
@@ -0,0 +1,29 @@
+package dev.kejona.crossplatforms;
+
+import org.junit.jupiter.api.Test;
+
+import static dev.kejona.crossplatforms.utils.ParseUtils.downSize;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class ParseUtilsTest {
+
+ @Test
+ public void testDownSize() {
+ assertEquals("1.2", downSize("1.2"));
+ assertEquals("3.14", downSize("3.14"));
+ assertEquals("-5.56", downSize("-5.56"));
+
+ assertEquals("3", downSize("3.0"));
+ assertEquals("14", downSize("14.00"));
+ assertEquals("-16", downSize("-16.000000000"));
+ assertEquals("256", downSize("256.0000000000000000000"));
+
+ assertEquals("165", downSize("165"));
+ assertEquals("-100", downSize("-100"));
+
+ assertThrows(Exception.class, () -> downSize(null));
+ assertThrows(Exception.class, () -> downSize(""));
+ assertThrows(Exception.class, () -> downSize(" "));
+ }
+}
diff --git a/core/src/test/java/dev/kejona/crossplatforms/config/keyedserializer/KeyedTypeListSerializerTest.java b/core/src/test/java/dev/kejona/crossplatforms/config/keyedserializer/KeyedTypeListSerializerTest.java
deleted file mode 100644
index 85621911..00000000
--- a/core/src/test/java/dev/kejona/crossplatforms/config/keyedserializer/KeyedTypeListSerializerTest.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package dev.kejona.crossplatforms.config.keyedserializer;
-
-import com.google.common.collect.ImmutableList;
-import com.google.inject.Guice;
-import dev.kejona.crossplatforms.TestModule;
-import dev.kejona.crossplatforms.serialize.KeyedTypeListSerializer;
-import dev.kejona.crossplatforms.serialize.KeyedTypeSerializer;
-import dev.kejona.crossplatforms.utils.ConfigurateUtils;
-import dev.kejona.crossplatforms.utils.FileUtils;
-import io.leangen.geantyref.TypeToken;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.io.TempDir;
-import org.spongepowered.configurate.ConfigurateException;
-import org.spongepowered.configurate.ConfigurationNode;
-import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-
-public class KeyedTypeListSerializerTest {
-
- private static final TypeToken> messageListType = new TypeToken>() {};
-
- @TempDir
- private static File directory;
-
- private final KeyedTypeSerializer messageSerializer = new KeyedTypeSerializer<>(Guice.createInjector(new TestModule()));
- private final YamlConfigurationLoader loader;
-
- public KeyedTypeListSerializerTest() throws IOException {
- messageSerializer.registerSimpleType("message", String.class, SingleMessage.class);
- messageSerializer.registerType("messages", MultiMessage.class);
-
- File config = FileUtils.fileOrCopiedFromResource(new File(directory, "KeyedTypeConfig.yml"));
- YamlConfigurationLoader.Builder loaderBuilder = ConfigurateUtils.loaderBuilder(config);
- loaderBuilder.defaultOptions(opts -> (opts.serializers(builder -> {
- builder.registerExact(Message.class, messageSerializer);
- builder.register(new TypeToken>() {}, new KeyedTypeListSerializer<>(messageSerializer));
- })));
- loader = loaderBuilder.build();
- }
-
- @Test
- public void testDeserialize() throws ConfigurateException {
- ConfigurationNode actions = loader.load().node("actions");
- Assertions.assertFalse(actions.virtual());
- Assertions.assertTrue(actions.isMap());
- List actualMessages = actions.get(messageListType);
-
- SingleMessage single = new SingleMessage("[WARN] Hello");
- MultiMessage list = new MultiMessage("[INFO]", ImmutableList.of("One", "Two", "Three"));
- List expectedMessages = ImmutableList.of(single, list);
-
- Assertions.assertEquals(expectedMessages, actualMessages);
- }
-
- @Test
- public void testSerialize() throws ConfigurateException {
- ConfigurationNode actions = loader.load().node("actions");
- ConfigurationNode copy = actions.copy();
-
- List actualMessages = actions.get(messageListType);
- Objects.requireNonNull(actualMessages);
-
- copy.set(messageListType, actualMessages);
- Assertions.assertEquals(actions, copy);
-
- List modifiedMessages = new ArrayList<>(actualMessages);
- modifiedMessages.add(new SingleMessage("greetings"));
- copy.set(messageListType, modifiedMessages);
- Assertions.assertNotEquals(actions, copy);
- }
-}
diff --git a/core/src/test/java/dev/kejona/crossplatforms/config/keyedserializer/KeyedTypeSerializerTest.java b/core/src/test/java/dev/kejona/crossplatforms/config/keyedserializer/KeyedTypeSerializerTest.java
deleted file mode 100644
index 68b1006d..00000000
--- a/core/src/test/java/dev/kejona/crossplatforms/config/keyedserializer/KeyedTypeSerializerTest.java
+++ /dev/null
@@ -1,74 +0,0 @@
-package dev.kejona.crossplatforms.config.keyedserializer;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.inject.Guice;
-import dev.kejona.crossplatforms.TestModule;
-import dev.kejona.crossplatforms.serialize.KeyedTypeSerializer;
-import dev.kejona.crossplatforms.utils.ConfigurateUtils;
-import dev.kejona.crossplatforms.utils.FileUtils;
-import io.leangen.geantyref.TypeToken;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.io.TempDir;
-import org.spongepowered.configurate.ConfigurateException;
-import org.spongepowered.configurate.ConfigurationNode;
-import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-
-public class KeyedTypeSerializerTest {
-
- private static final TypeToken