From 1053a48aa4697886cd2a7de36bb6e5ef8a728c2c Mon Sep 17 00:00:00 2001 From: Xander Date: Tue, 22 Nov 2022 21:31:06 +0000 Subject: Create NbtConfigInstance.java --- .../isxander/yacl/config/NbtConfigInstance.java | 274 +++++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 src/main/java/dev/isxander/yacl/config/NbtConfigInstance.java (limited to 'src') diff --git a/src/main/java/dev/isxander/yacl/config/NbtConfigInstance.java b/src/main/java/dev/isxander/yacl/config/NbtConfigInstance.java new file mode 100644 index 0000000..5749695 --- /dev/null +++ b/src/main/java/dev/isxander/yacl/config/NbtConfigInstance.java @@ -0,0 +1,274 @@ +package dev.isxander.yacl.config; + +import dev.isxander.yacl.impl.utils.YACLConstants; +import net.minecraft.nbt.*; + +import java.awt.*; +import java.io.DataOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.reflect.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; + +/** + * Uses {@link net.minecraft.nbt} to serialize and deserialize to and from an NBT file. + * Data can be written as compressed GZIP or uncompressed NBT. + * + * You can optionally provide custom implementations for handling certain classes if the default + * handling fails with {@link NbtSerializer} + * + * @param config data type + */ +@SuppressWarnings("unchecked") +public class NbtConfigInstance extends ConfigInstance { + private final Path path; + private final boolean compressed; + private final NbtSerializerHolder nbtSerializerHolder; + + /** + * Constructs an instance with compression on + * + * @param configClass config data type class + * @param path file to write nbt to + */ + public NbtConfigInstance(Class configClass, Path path) { + this(configClass, path, holder -> holder, true); + } + + /** + * @param configClass config data type class + * @param path file to write nbt to + * @param serializerHolderBuilder allows you to add custom serializers + * @param compressed whether to compress the NBT + */ + public NbtConfigInstance(Class configClass, Path path, UnaryOperator serializerHolderBuilder, boolean compressed) { + super(configClass); + this.path = path; + this.compressed = compressed; + this.nbtSerializerHolder = serializerHolderBuilder.apply(new NbtSerializerHolder()); + } + + @Override + public void save() { + YACLConstants.LOGGER.info("Saving {}...", getConfigClass().getSimpleName()); + + NbtCompound nbt; + try { + nbt = (NbtCompound) serializeObject(getConfig(), nbtSerializerHolder, field -> field.isAnnotationPresent(ConfigEntry.class)); + } catch (IllegalAccessException e) { + YACLConstants.LOGGER.error("Failed to convert '{}' -> NBT", getConfigClass().getName(), e); + return; + } + + try(FileOutputStream fos = new FileOutputStream(path.toFile())) { + if (Files.notExists(path)) + Files.createFile(path); + + if (compressed) + NbtIo.writeCompressed(nbt, fos); + else + NbtIo.write(nbt, new DataOutputStream(fos)); + } catch (IOException e) { + YACLConstants.LOGGER.error("Failed to write NBT to '{}'", path, e); + } + } + + @Override + public void load() { + if (Files.notExists(path)) { + save(); + return; + } + + YACLConstants.LOGGER.info("Loading {}...", getConfigClass().getSimpleName()); + NbtCompound nbt; + try { + nbt = compressed ? NbtIo.readCompressed(path.toFile()) : NbtIo.read(path.toFile()); + } catch (IOException e) { + YACLConstants.LOGGER.error("Failed to read NBT file '{}'", path, e); + return; + } + + try { + setConfig(deserializeObject(nbt, getConfigClass(), nbtSerializerHolder, field -> field.isAnnotationPresent(ConfigEntry.class))); + } catch (InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException e) { + YACLConstants.LOGGER.error("Failed to convert NBT -> '{}'", getConfigClass().getName(), e); + } + } + + public Path getPath() { + return this.path; + } + + public boolean isUsingCompression() { + return this.compressed; + } + + private static NbtElement serializeObject(Object object, NbtSerializerHolder serializerHolder, Predicate topLevelPredicate) throws IllegalAccessException { + if (serializerHolder.hasSerializer(object.getClass())) { + return serializerHolder.serialize(object); + } + else if (object instanceof Object[] ol) { + NbtList nbtList = new NbtList(); + for (Object obj : ol) + nbtList.add(serializeObject(obj, serializerHolder, field -> true)); + return nbtList; + } else { + NbtCompound compound = new NbtCompound(); + Field[] fields = object.getClass().getDeclaredFields(); + for (Field field : fields) { + if (Modifier.isStatic(field.getModifiers()) || !topLevelPredicate.test(field)) + continue; + + System.out.println(field.getName()); + field.setAccessible(true); + + String key = toCamelCase(field.getName()); + NbtElement value = serializeObject(field.get(object), serializerHolder, f -> true); + compound.put(key, value); + } + + return compound; + } + } + + private static T deserializeObject(NbtElement element, Class type, NbtSerializerHolder serializerHolder, Predicate topLevelPredicate) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + if (serializerHolder.hasSerializer(type)) + return serializerHolder.get(type).deserialize(element, type); + else if (type == Array.class) { + List list = new ArrayList<>(); + + Class arrayType = Array.newInstance(type.getComponentType(), 0).getClass(); + NbtList nbtList = (NbtList) element; + for (NbtElement nbtElement : nbtList) { + list.add(deserializeObject(nbtElement, arrayType, serializerHolder, field -> true)); + } + + return (T) list.toArray(); + } else { + if (!(element instanceof NbtCompound compound)) + throw new IllegalStateException("Cannot deserialize " + type.getName()); + + T object = type.getConstructor().newInstance(); + Field[] fields = type.getDeclaredFields(); + for (Field field : fields) { + if (Modifier.isStatic(field.getModifiers()) || !topLevelPredicate.test(field)) + continue; + + field.setAccessible(true); + String key = toCamelCase(field.getName()); + if (!compound.contains(key)) + continue; + field.set(object, deserializeObject(compound.get(key), field.getType(), serializerHolder, f -> true)); + } + + return object; + } + } + + /* shamelessly stolen from gson */ + private static String toCamelCase(String name) { + StringBuilder translation = new StringBuilder(); + for (int i = 0, length = name.length(); i < length; i++) { + char character = name.charAt(i); + if (Character.isUpperCase(character) && translation.length() != 0) { + translation.append('_'); + } + translation.append(character); + } + return translation.toString().toLowerCase(Locale.ENGLISH); + } + + public static class NbtSerializerHolder { + private final Map, NbtSerializer> serializerMap = new HashMap<>(); + + private NbtSerializerHolder() { + register(boolean.class, NbtSerializer.simple(b -> b ? NbtByte.ONE : NbtByte.ZERO, nbt -> nbt.byteValue() != 0)); + register(Boolean.class, NbtSerializer.simple(b -> b ? NbtByte.ONE : NbtByte.ZERO, nbt -> nbt.byteValue() != 0)); + register(int.class, NbtSerializer.simple(NbtInt::of, NbtInt::intValue)); + register(Integer.class, NbtSerializer.simple(NbtInt::of, NbtInt::intValue));register(int[].class, NbtSerializer.simple(NbtIntArray::new, NbtIntArray::getIntArray)); + register(float.class, NbtSerializer.simple(NbtFloat::of, NbtFloat::floatValue)); + register(Float.class, NbtSerializer.simple(NbtFloat::of, NbtFloat::floatValue)); + register(double.class, NbtSerializer.simple(NbtDouble::of, NbtDouble::doubleValue)); + register(Double.class, NbtSerializer.simple(NbtDouble::of, NbtDouble::doubleValue)); + register(short.class, NbtSerializer.simple(NbtShort::of, NbtShort::shortValue)); + register(Short.class, NbtSerializer.simple(NbtShort::of, NbtShort::shortValue)); + register(byte.class, NbtSerializer.simple(NbtByte::of, NbtByte::byteValue)); + register(Byte.class, NbtSerializer.simple(NbtByte::of, NbtByte::byteValue)); + register(byte[].class, NbtSerializer.simple(NbtByteArray::new, NbtByteArray::getByteArray)); + register(long.class, NbtSerializer.simple(NbtLong::of, NbtLong::longValue)); + register(Long.class, NbtSerializer.simple(NbtLong::of, NbtLong::longValue)); + register(long[].class, NbtSerializer.simple(NbtLongArray::new, NbtLongArray::getLongArray)); + register(String.class, NbtSerializer.simple(NbtString::of, NbtString::asString)); + register(Enum.class, NbtSerializer.simple(e -> NbtString.of(e.name()), (nbt, type) -> Arrays.stream(type.getEnumConstants()).filter(e -> e.name().equals(nbt.asString())).findFirst().orElseThrow())); + + register(Color.class, new ColorSerializer()); + } + + public NbtSerializerHolder register(Class clazz, NbtSerializer serializer) { + serializerMap.put(clazz, serializer); + return this; + } + + public NbtSerializer get(Class clazz) { + return (NbtSerializer) search(clazz).findFirst().orElseThrow().getValue(); + } + + public boolean hasSerializer(Class clazz) { + return search(clazz).findAny().isPresent(); + } + + public NbtElement serialize(Object object) { + return ((NbtSerializer) get(object.getClass())).serialize(object); + } + + private Stream, NbtSerializer>> search(Class type) { + return serializerMap.entrySet().stream().filter(entry -> entry.getKey().isAssignableFrom(type)); + } + } + + public interface NbtSerializer { + NbtElement serialize(T object); + + T deserialize(NbtElement element, Class type); + + static NbtSerializer simple(Function serializer, Function deserializer) { + return simple(serializer, (nbt, type) -> deserializer.apply(nbt)); + } + + static NbtSerializer simple(Function serializer, BiFunction, T> deserializer) { + return new NbtSerializer<>() { + @Override + public NbtElement serialize(T object) { + return serializer.apply(object); + } + + @Override + public T deserialize(NbtElement element, Class type) { + return deserializer.apply((U) element, type); + } + }; + } + } + + public static class ColorSerializer implements NbtSerializer { + + @Override + public NbtElement serialize(Color object) { + return NbtInt.of(object.getRGB()); + } + + @Override + public Color deserialize(NbtElement element, Class type) { + return new Color(((NbtInt) element).intValue(), true); + } + } +} -- cgit From c0fea65be6c7527539196da2d2e11ce12c574fc5 Mon Sep 17 00:00:00 2001 From: Xander Date: Tue, 22 Nov 2022 21:36:18 +0000 Subject: annotate test config with ConfigEntry --- .../dev/isxander/yacl/test/config/ConfigData.java | 32 ++++++++++++---------- 1 file changed, 17 insertions(+), 15 deletions(-) (limited to 'src') diff --git a/src/testmod/java/dev/isxander/yacl/test/config/ConfigData.java b/src/testmod/java/dev/isxander/yacl/test/config/ConfigData.java index d0d47b1..35e57dd 100644 --- a/src/testmod/java/dev/isxander/yacl/test/config/ConfigData.java +++ b/src/testmod/java/dev/isxander/yacl/test/config/ConfigData.java @@ -1,25 +1,27 @@ package dev.isxander.yacl.test.config; +import dev.isxander.yacl.config.ConfigEntry; + import java.awt.*; public class ConfigData { - public boolean booleanToggle = false; - public boolean customBooleanToggle = false; - public boolean tickbox = false; - public int intSlider = 0; - public double doubleSlider = 0; - public float floatSlider = 0; - public long longSlider = 0; - public String textField = "Hello"; - public Color colorOption = Color.red; - public Alphabet enumOption = Alphabet.A; + @ConfigEntry public boolean booleanToggle = false; + @ConfigEntry public boolean customBooleanToggle = false; + @ConfigEntry public boolean tickbox = false; + @ConfigEntry public int intSlider = 0; + @ConfigEntry public double doubleSlider = 0; + @ConfigEntry public float floatSlider = 0; + @ConfigEntry public long longSlider = 0; + @ConfigEntry public String textField = "Hello"; + @ConfigEntry public Color colorOption = Color.red; + @ConfigEntry public Alphabet enumOption = Alphabet.A; - public boolean groupTestRoot = false; - public boolean groupTestFirstGroup = false; - public boolean groupTestFirstGroup2 = false; - public boolean groupTestSecondGroup = false; + @ConfigEntry public boolean groupTestRoot = false; + @ConfigEntry public boolean groupTestFirstGroup = false; + @ConfigEntry public boolean groupTestFirstGroup2 = false; + @ConfigEntry public boolean groupTestSecondGroup = false; - public int scrollingSlider = 0; + @ConfigEntry public int scrollingSlider = 0; public enum Alphabet { A, B, C -- cgit