package emu.grasscutter.utils; import com.google.gson.Gson; import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; import emu.grasscutter.data.common.DynamicFloat; import it.unimi.dsi.fastutil.floats.FloatArrayList; import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntList; import lombok.val; import java.io.IOException; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashMap; import java.util.Objects; public class JsonAdapters { static class DynamicFloatAdapter extends TypeAdapter { @Override public DynamicFloat read(JsonReader reader) throws IOException { switch (reader.peek()) { case STRING: return new DynamicFloat(reader.nextString()); case NUMBER: return new DynamicFloat((float) reader.nextDouble()); case BOOLEAN: return new DynamicFloat(reader.nextBoolean()); case BEGIN_ARRAY: reader.beginArray(); val opStack = new ArrayList(); while (reader.hasNext()) { opStack.add( switch (reader.peek()) { case STRING -> new DynamicFloat.StackOp(reader.nextString()); case NUMBER -> new DynamicFloat.StackOp((float) reader.nextDouble()); case BOOLEAN -> new DynamicFloat.StackOp(reader.nextBoolean()); default -> throw new IOException( "Invalid DynamicFloat definition - " + reader.peek().name()); }); } reader.endArray(); return new DynamicFloat(opStack); default: throw new IOException("Invalid DynamicFloat definition - " + reader.peek().name()); } } @Override public void write(JsonWriter writer, DynamicFloat f) {} } static class IntListAdapter extends TypeAdapter { @Override public IntList read(JsonReader reader) throws IOException { if (Objects.requireNonNull(reader.peek()) == JsonToken.BEGIN_ARRAY) { reader.beginArray(); val i = new IntArrayList(); while (reader.hasNext()) i.add(reader.nextInt()); reader.endArray(); i.trim(); // We might have a ton of these from resources and almost all of them // immutable, don't overprovision! return i; } throw new IOException("Invalid IntList definition - " + reader.peek().name()); } @Override public void write(JsonWriter writer, IntList l) throws IOException { writer.beginArray(); for (val i : l) // .forEach() doesn't appreciate exceptions writer.value(i); writer.endArray(); } } public static class ByteArrayAdapter extends TypeAdapter { @Override public void write(JsonWriter out, byte[] value) throws IOException { out.value(Utils.base64Encode(value)); } @Override public byte[] read(JsonReader in) throws IOException { return Utils.base64Decode(in.nextString()); } } static class GridPositionAdapter extends TypeAdapter { @Override public void write(JsonWriter out, GridPosition value) throws IOException { out.value("(" + value.getX() + ", " + value.getZ() + ", " + value.getWidth() + ")"); } @Override public GridPosition read(JsonReader in) throws IOException { if (in.peek() != JsonToken.STRING) throw new IOException("Invalid GridPosition definition - " + in.peek().name()); // GridPosition follows the format of: (x, y, z). // Flatten to (x,y,z) for easier parsing. var str = in.nextString().replace("(", "").replace(")", "").replace(" ", ""); var split = str.split(","); if (split.length != 3) throw new IOException("Invalid GridPosition definition - " + in.peek().name()); return new GridPosition( Integer.parseInt(split[0]), Integer.parseInt(split[1]), Integer.parseInt(split[2])); } } static class PositionAdapter extends TypeAdapter { @Override public Position read(JsonReader reader) throws IOException { switch (reader.peek()) { case BEGIN_ARRAY: // "pos": [x,y,z] reader.beginArray(); val array = new FloatArrayList(3); while (reader.hasNext()) array.add(reader.nextInt()); reader.endArray(); return new Position(array); case BEGIN_OBJECT: // "pos": {"x": x, "y": y, "z": z} float x = 0f; float y = 0f; float z = 0f; reader.beginObject(); for (var next = reader.peek(); next != JsonToken.END_OBJECT; next = reader.peek()) { val name = reader.nextName(); switch (name) { case "x", "X", "_x" -> x = (float) reader.nextDouble(); case "y", "Y", "_y" -> y = (float) reader.nextDouble(); case "z", "Z", "_z" -> z = (float) reader.nextDouble(); default -> throw new IOException("Invalid field in Position definition - " + name); } } reader.endObject(); return new Position(x, y, z); default: throw new IOException("Invalid Position definition - " + reader.peek().name()); } } @Override public void write(JsonWriter writer, Position i) throws IOException { writer.beginArray(); writer.value(i.getX()); writer.value(i.getY()); writer.value(i.getZ()); writer.endArray(); } } static class EnumTypeAdapterFactory implements TypeAdapterFactory { @SuppressWarnings("unchecked") public TypeAdapter create(Gson gson, TypeToken type) { Class enumClass = (Class) type.getRawType(); if (!enumClass.isEnum()) return null; // Make mappings of (string) names to enum constants val map = new HashMap(); val enumConstants = enumClass.getEnumConstants(); for (val constant : enumConstants) map.put(constant.toString(), constant); // If the enum also has a numeric value, map those to the constants too // System.out.println("Looking for enum value field"); for (Field f : enumClass.getDeclaredFields()) { if (switch (f.getName()) { case "value", "id" -> true; default -> false; }) { // System.out.println("Enum value field found - " + f.getName()); boolean acc = f.isAccessible(); f.setAccessible(true); try { for (val constant : enumConstants) map.put(String.valueOf(f.getInt(constant)), constant); } catch (IllegalAccessException e) { // System.out.println("Failed to access enum id field."); } f.setAccessible(acc); break; } } return new TypeAdapter() { public T read(JsonReader reader) throws IOException { switch (reader.peek()) { case STRING: return map.get(reader.nextString()); case NUMBER: return map.get(String.valueOf(reader.nextInt())); default: throw new IOException("Invalid Enum definition - " + reader.peek().name()); } } public void write(JsonWriter writer, T value) throws IOException { writer.value(value.toString()); } }; } } }