diff --git a/build.gradle b/build.gradle index fd73ed5c..1cdf5eec 100644 --- a/build.gradle +++ b/build.gradle @@ -42,6 +42,7 @@ dependencies { modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" shadow('ca.spottedleaf:concurrentutil:0.0.1-SNAPSHOT') + shadow('org.yaml:snakeyaml:2.2') } processResources { diff --git a/src/main/java/ca/spottedleaf/moonrise/common/config/MoonriseConfig.java b/src/main/java/ca/spottedleaf/moonrise/common/config/MoonriseConfig.java new file mode 100644 index 00000000..60ea7bef --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/config/MoonriseConfig.java @@ -0,0 +1,159 @@ +package ca.spottedleaf.moonrise.common.config; + +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapterRegistry; +import ca.spottedleaf.moonrise.common.config.annotation.Adaptable; +import ca.spottedleaf.moonrise.common.config.annotation.Serializable; +import ca.spottedleaf.moonrise.common.config.type.Duration; +import java.util.concurrent.TimeUnit; + +@Adaptable +public final class MoonriseConfig { + + @Serializable( + comment = """ + Do not change, used internally. + """ + ) + public int version = 1; + + @Serializable + public ChunkLoading chunkLoading = new ChunkLoading(); + + @Adaptable + public static final class ChunkLoading { + + @Serializable( + comment = """ + Chunk loading/generation/sending rate targets for the chunk system. These values are the + maximum rates at which the player chunk loader will attempt to load/generate/send chunks to + players. Actual resulting rates will depend on hardware. + """ + ) + public Basic basic = new Basic(); + + + @Adaptable + public static final class Basic { + @Serializable( + comment = """ + The maximum number of chunks to send to any given player, per second. + """ + ) + public double playerMaxSendRate = -1.0; + + @Serializable( + comment = """ + The maximum number of chunks to load from disk for any given player, per second. + """ + ) + public double playerMaxLoadRate = -1.0; + + @Serializable( + comment = """ + The maximum number of chunks to generate for any given player, per second. + """ + ) + public double playerMaxGenRate = -1.0; + } + + @Serializable( + comment = """ + Advanced configuration options for player chunk loading. You shouldn't be touching these + unless you have a reason. + """ + ) + public Advanced advanced = new Advanced(); + + @Adaptable + public static final class Advanced { + + @Serializable( + comment = """ + Whether to avoid sending chunks to players who have a view distance + configured lower than the server's. + """ + ) + public boolean autoConfigSendDistance = true; + + @Serializable( + comment = """ + The maximum amount of pending chunk loads per player. If + this value is less-than 1, then the player chunk loader will + automatically determine a value. + + This value should be used to tune the saturation of the chunk system. + """ + ) + public int playerMaxConcurrentChunkLoads = 0; + + @Serializable( + comment = """ + The maximum amount of pending chunk generations per player. If + this value is less-than 1, then the player chunk loader will + automatically determine a value. + + This value should be used to tune the saturation of the chunk system. + """ + ) + public int playerMaxConcurrentChunkGenerates = 0; + } + } + + @Serializable + public ChunkSaving chunkSaving = new ChunkSaving(); + + @Adaptable + public static final class ChunkSaving { + + @Serializable( + comment = """ + The interval at which chunks should be incrementally autosaved. + """ + ) + public Duration autoSaveInterval = Duration.parse("5m"); + + @Serializable( + comment = """ + The maximum number of chunks to incrementally autosave each tick. If + the value is <= 0, then no chunks will be incrementally saved. + """ + ) + public int maxAutoSaveChunksPerTick = 12; + } + + @Serializable( + comment = """ + Set the number of shared worker threads to be used by chunk rendering, + chunk loading, chunk generation. If the value is <= 0, then the number + of threads will automatically be determined. + """ + ) + public int workerThreads = -1; + + @Serializable + public ChunkSystem chunkSystem = new ChunkSystem(); + + @Adaptable + public static final class ChunkSystem { + + @Serializable( + comment = """ + Set the number of threads dedicated to RegionFile I/O operations. + If the value is <= 0, then the number of threads used is 1. Configuring + a higher value than 1 is only recommended on SSDs (HDDs scale negatively) + and when you have determined that I/O is the bottleneck for chunk loading/saving. + """ + ) + public int ioThreads = -1; + + @Serializable( + comment = """ + Whether to run generation population in parallel. By default this is set to false, + as mods affecting world gen are not safe to run in parallel. If you have no mods affecting + gen and are saturating the population generation (~10 threads of the worker pool generating + chunks), you may set this to true to possibly increase generation speed. + """ + ) + public boolean populationGenParallelism = false; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/config/PlaceholderConfig.java b/src/main/java/ca/spottedleaf/moonrise/common/config/PlaceholderConfig.java deleted file mode 100644 index a556b6b4..00000000 --- a/src/main/java/ca/spottedleaf/moonrise/common/config/PlaceholderConfig.java +++ /dev/null @@ -1,21 +0,0 @@ -package ca.spottedleaf.moonrise.common.config; - -public final class PlaceholderConfig { - - public static double chunkLoadingBasic$playerMaxChunkSendRate = -1.0; - public static double chunkLoadingBasic$playerMaxChunkLoadRate = -1.0; - public static double chunkLoadingBasic$playerMaxChunkGenerateRate = -1.0; - - public static boolean chunkLoadingAdvanced$autoConfigSendDistance = true; - public static int chunkLoadingAdvanced$playerMaxConcurrentChunkLoads = 0; - public static int chunkLoadingAdvanced$playerMaxConcurrentChunkGenerates = 0; - - public static int autoSaveInterval = 60 * 5 * 20; // 5 mins - public static int maxAutoSaveChunksPerTick = 12; - - public static int workerThreads = -1; - - public static int chunkSystemIOThreads = -1; - public static String chunkSystemGenParallelism = "default"; - -} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/TypeAdapter.java b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/TypeAdapter.java new file mode 100644 index 00000000..56100797 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/TypeAdapter.java @@ -0,0 +1,11 @@ +package ca.spottedleaf.moonrise.common.config.adapter; + +import java.lang.reflect.Type; + +public abstract class TypeAdapter { + + public abstract T deserialize(final TypeAdapterRegistry registry, final Object input, final Type type); + + public abstract S serialize(final TypeAdapterRegistry registry, final T value, final Type type); + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/TypeAdapterRegistry.java b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/TypeAdapterRegistry.java new file mode 100644 index 00000000..05e2db31 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/TypeAdapterRegistry.java @@ -0,0 +1,269 @@ +package ca.spottedleaf.moonrise.common.config.adapter; + +import ca.spottedleaf.moonrise.common.config.adapter.collection.CollectionTypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.collection.ListTypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.collection.SortedMapTypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.collection.UnsortedMapTypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.primitive.BooleanTypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.primitive.ByteTypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.primitive.DoubleTypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.primitive.FloatTypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.primitive.IntegerTypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.primitive.LongTypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.primitive.ShortTypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.primitive.StringTypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.type.BigDecimalTypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.type.BigIntegerTypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.type.DurationTypeAdapter; +import ca.spottedleaf.moonrise.common.config.annotation.Adaptable; +import ca.spottedleaf.moonrise.common.config.annotation.Serializable; +import ca.spottedleaf.moonrise.common.config.type.Duration; +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +public final class TypeAdapterRegistry { + + private final Map, TypeAdapter> adapters = new HashMap<>(); + { + this.adapters.put(boolean.class, BooleanTypeAdapter.INSTANCE); + this.adapters.put(byte.class, ByteTypeAdapter.INSTANCE); + this.adapters.put(short.class, ShortTypeAdapter.INSTANCE); + this.adapters.put(int.class, IntegerTypeAdapter.INSTANCE); + this.adapters.put(long.class, LongTypeAdapter.INSTANCE); + this.adapters.put(float.class, FloatTypeAdapter.INSTANCE); + this.adapters.put(double.class, DoubleTypeAdapter.INSTANCE); + + this.adapters.put(Boolean.class, BooleanTypeAdapter.INSTANCE); + this.adapters.put(Byte.class, ByteTypeAdapter.INSTANCE); + this.adapters.put(Short.class, ShortTypeAdapter.INSTANCE); + this.adapters.put(Integer.class, IntegerTypeAdapter.INSTANCE); + this.adapters.put(Long.class, LongTypeAdapter.INSTANCE); + this.adapters.put(Float.class, FloatTypeAdapter.INSTANCE); + this.adapters.put(Double.class, DoubleTypeAdapter.INSTANCE); + + this.adapters.put(String.class, StringTypeAdapter.INSTANCE); + + this.adapters.put(Collection.class, CollectionTypeAdapter.INSTANCE); + this.adapters.put(List.class, ListTypeAdapter.INSTANCE); + this.adapters.put(Map.class, SortedMapTypeAdapter.SORTED_CASE_INSENSITIVE); + this.adapters.put(LinkedHashMap.class, SortedMapTypeAdapter.SORTED_CASE_INSENSITIVE); + + this.adapters.put(BigInteger.class, BigIntegerTypeAdapter.INSTANCE); + this.adapters.put(BigDecimal.class, BigDecimalTypeAdapter.INSTANCE); + this.adapters.put(Duration.class, DurationTypeAdapter.INSTANCE); + } + + public TypeAdapter putAdapter(final Class clazz, final TypeAdapter adapter) { + return this.adapters.put(clazz, adapter); + } + + public TypeAdapter getAdapter(final Class clazz) { + return this.adapters.get(clazz); + } + + public Object deserialize(final Object input, final Type type) { + TypeAdapter adapter = null; + if (type instanceof Class clazz) { + adapter = this.adapters.get(clazz); + } + if (adapter == null && (type instanceof ParameterizedType parameterizedType)) { + adapter = this.adapters.get((Class)parameterizedType.getRawType()); + } + + if (adapter == null) { + throw new IllegalArgumentException("No adapter for " + input.getClass() + " with type " + type); + } + + return ((TypeAdapter)adapter).deserialize(this, input, type); + } + + public Object serialize(final Object input, final Type type) { + TypeAdapter adapter = null; + if (type instanceof Class clazz) { + adapter = this.adapters.get(clazz); + } + if (adapter == null && (type instanceof ParameterizedType parameterizedType)) { + adapter = this.adapters.get((Class)parameterizedType.getRawType()); + } + if (adapter == null) { + adapter = this.adapters.get(input.getClass()); + } + + if (adapter == null) { + throw new IllegalArgumentException("No adapter for " + input.getClass() + " with type " + type); + } + + return ((TypeAdapter)adapter).serialize(this, input, type); + } + + public TypeAdapter> makeAdapter(final Class clazz) throws Exception { + final TypeAdapter> ret = new AutoTypeAdapter<>(this, clazz); + + this.putAdapter(clazz, ret); + + return ret; + } + + private static final class AutoTypeAdapter extends TypeAdapter> { + + private final TypeAdapterRegistry registry; + private final Constructor constructor; + private final SerializableField[] fields; + + public AutoTypeAdapter(final TypeAdapterRegistry registry, final Class clazz) throws Exception { + this.registry = registry; + this.constructor = clazz.getConstructor(); + this.fields = findSerializableFields(registry, clazz); + } + + private static TypeAdapter findOrMakeAdapter(final TypeAdapterRegistry registry, final Class clazz) throws Exception { + final TypeAdapter ret = registry.getAdapter(clazz); + if (ret != null) { + return ret; + } + + for (final Annotation annotation : clazz.getAnnotations()) { + if (annotation instanceof Adaptable adaptable) { + return registry.makeAdapter(clazz); + } + } + + throw new IllegalArgumentException("No type adapter for " + clazz + " (Forgot @Adaptable?)"); + } + + private static String makeSerializedKey(final String input) { + final StringBuilder ret = new StringBuilder(); + + for (final char c : input.toCharArray()) { + if (!Character.isUpperCase(c)) { + ret.append(c); + continue; + } + ret.append('-'); + ret.append(Character.toLowerCase(c)); + } + + return ret.toString(); + } + + private static record SerializableField( + Field field, + boolean required, + String comment, + TypeAdapter adapter, + boolean serialize, + String serializedKey + ) {} + + private static SerializableField[] findSerializableFields(final TypeAdapterRegistry registry, Class clazz) throws Exception { + final List ret = new ArrayList<>(); + do { + for (final Field field : clazz.getDeclaredFields()) { + field.setAccessible(true); + + for (final Annotation annotation : field.getAnnotations()) { + if (!(annotation instanceof Serializable serializable)) { + continue; + } + + final TypeAdapter adapter; + + if (serializable.adapter() != TypeAdapter.class) { + adapter = serializable.adapter().getConstructor().newInstance(); + } else { + adapter = findOrMakeAdapter(registry, field.getType()); + } + + ret.add(new SerializableField( + field, serializable.required(), serializable.comment(), adapter, + serializable.serialize(), makeSerializedKey(field.getName()) + )); + } + } + } while ((clazz = clazz.getSuperclass()) != Object.class); + + ret.sort((final SerializableField c1, final SerializableField c2) -> { + return c1.serializedKey.compareTo(c2.serializedKey); + }); + + return ret.toArray(new SerializableField[0]); + } + + @Override + public T deserialize(final TypeAdapterRegistry registry, final Object input, final Type type) { + if (!(input instanceof Map inputMap)) { + throw new IllegalArgumentException("Not a map type: " + input.getClass()); + } + + try { + final T ret = this.constructor.newInstance(); + + for (final SerializableField field : this.fields) { + final Object fieldValue = inputMap.get(field.serializedKey); + + if (fieldValue == null) { + continue; + } + + field.field.set(ret, field.adapter.deserialize(registry, fieldValue, field.field.getGenericType())); + } + + return ret; + } catch (final Exception ex) { + throw new RuntimeException(ex); + } + } + + @Override + public Map serialize(final TypeAdapterRegistry registry, final T value, final Type type) { + final LinkedHashMap ret = new LinkedHashMap<>(); + + for (final SerializableField field : this.fields) { + if (!field.serialize) { + continue; + } + + final Object fieldValue; + try { + fieldValue = field.field.get(value); + } catch (final Exception ex) { + throw new RuntimeException(ex); + } + + if (fieldValue != null) { + ret.put( + field.comment.isBlank() ? field.serializedKey : new CommentedData(field.comment, field.serializedKey), + ((TypeAdapter)field.adapter).serialize( + registry, fieldValue, field.field.getGenericType() + ) + ); + } + } + + return ret; + } + } + + public static final class CommentedData { + + public final String comment; + public final Object data; + + public CommentedData(final String comment, final Object data) { + this.comment = comment; + this.data = data; + } + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/collection/CollectionTypeAdapter.java b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/collection/CollectionTypeAdapter.java new file mode 100644 index 00000000..832fc45a --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/collection/CollectionTypeAdapter.java @@ -0,0 +1,46 @@ +package ca.spottedleaf.moonrise.common.config.adapter.collection; + +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapterRegistry; +import ca.spottedleaf.moonrise.common.config.adapter.primitive.StringTypeAdapter; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public final class CollectionTypeAdapter extends TypeAdapter, List> { + + public static final CollectionTypeAdapter INSTANCE = new CollectionTypeAdapter(); + + @Override + public Collection deserialize(final TypeAdapterRegistry registry, final Object input, final Type type) { + if (!(type instanceof ParameterizedType parameterizedType)) { + throw new IllegalArgumentException("Collection field must specify generic type"); + } + final Type elementType = parameterizedType.getActualTypeArguments()[0]; + if (input instanceof Collection collection) { + final List ret = new ArrayList<>(collection.size()); + + for (final Object v : collection) { + ret.add(registry.deserialize(v, elementType)); + } + + return ret; + } + throw new IllegalArgumentException("Not a collection type: " + input.getClass()); + } + + @Override + public List serialize(final TypeAdapterRegistry registry, final Collection value, final Type type) { + final List ret = new ArrayList<>(value.size()); + + final Type elementType = type instanceof ParameterizedType parameterizedType ? parameterizedType.getActualTypeArguments()[0] : null; + + for (final Object v : value) { + ret.add(registry.serialize(v, elementType == null ? v.getClass() : elementType)); + } + + return ret; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/collection/ListTypeAdapter.java b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/collection/ListTypeAdapter.java new file mode 100644 index 00000000..9dea6224 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/collection/ListTypeAdapter.java @@ -0,0 +1,45 @@ +package ca.spottedleaf.moonrise.common.config.adapter.collection; + +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapterRegistry; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public final class ListTypeAdapter extends TypeAdapter, List> { + + public static final ListTypeAdapter INSTANCE = new ListTypeAdapter(); + + @Override + public List deserialize(final TypeAdapterRegistry registry, final Object input, final Type type) { + if (!(type instanceof ParameterizedType parameterizedType)) { + throw new IllegalArgumentException("Collection field must specify generic type"); + } + final Type elementType = parameterizedType.getActualTypeArguments()[0]; + if (input instanceof Collection collection) { + final List ret = new ArrayList<>(collection.size()); + + for (final Object v : collection) { + ret.add(registry.deserialize(v, elementType)); + } + + return ret; + } + throw new IllegalArgumentException("Not a collection type: " + input.getClass()); + } + + @Override + public List serialize(final TypeAdapterRegistry registry, final List value, final Type type) { + final List ret = new ArrayList<>(value.size()); + + final Type elementType = type instanceof ParameterizedType parameterizedType ? parameterizedType.getActualTypeArguments()[0] : null; + + for (final Object v : value) { + ret.add(registry.serialize(v, elementType)); + } + + return ret; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/collection/SortedMapTypeAdapter.java b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/collection/SortedMapTypeAdapter.java new file mode 100644 index 00000000..0576640f --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/collection/SortedMapTypeAdapter.java @@ -0,0 +1,59 @@ +package ca.spottedleaf.moonrise.common.config.adapter.collection; + +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapterRegistry; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.TreeMap; + +public final class SortedMapTypeAdapter extends TypeAdapter, Map> { + + public static final SortedMapTypeAdapter SORTED_CASE_INSENSITIVE = new SortedMapTypeAdapter(String.CASE_INSENSITIVE_ORDER); + public static final SortedMapTypeAdapter SORTED_CASE_SENSITIVE = new SortedMapTypeAdapter(null); + + private final Comparator keyComparator; + + public SortedMapTypeAdapter(final Comparator keyComparator) { + this.keyComparator = keyComparator; + } + + @Override + public Map deserialize(final TypeAdapterRegistry registry, final Object input, final Type type) { + if (!(type instanceof ParameterizedType parameterizedType)) { + throw new IllegalArgumentException("Collection field must specify generic type"); + } + final Type valueType = parameterizedType.getActualTypeArguments()[1]; + if (input instanceof Map inputMap) { + final Map castedInput = (Map)inputMap; + + final TreeMap ret = new TreeMap<>(this.keyComparator); + + for (final Map.Entry entry : castedInput.entrySet()) { + ret.put(entry.getKey(), registry.deserialize(entry.getValue(), valueType)); + } + + // transform to linked so that get() is O(1) + return new LinkedHashMap<>(ret); + } + + throw new IllegalArgumentException("Not a map type: " + input.getClass()); + } + + @Override + public Map serialize(final TypeAdapterRegistry registry, final Map value, final Type type) { + final TreeMap ret = new TreeMap<>(this.keyComparator); + + final Type valueType = type instanceof ParameterizedType parameterizedType ? parameterizedType.getActualTypeArguments()[1] : null; + + for (final Map.Entry entry : value.entrySet()) { + ret.put(entry.getKey(), registry.serialize(entry.getValue(), valueType)); + } + + // transform to linked so that get() is O(1) + return new LinkedHashMap<>(ret); + } + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/collection/UnsortedMapTypeAdapter.java b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/collection/UnsortedMapTypeAdapter.java new file mode 100644 index 00000000..dd172cab --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/collection/UnsortedMapTypeAdapter.java @@ -0,0 +1,47 @@ +package ca.spottedleaf.moonrise.common.config.adapter.collection; + +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapterRegistry; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.LinkedHashMap; +import java.util.Map; + +public final class UnsortedMapTypeAdapter extends TypeAdapter, Map> { + + public static final UnsortedMapTypeAdapter INSTANCE = new UnsortedMapTypeAdapter(); + + @Override + public Map deserialize(final TypeAdapterRegistry registry, final Object input, final Type type) { + if (!(type instanceof ParameterizedType parameterizedType)) { + throw new IllegalArgumentException("Collection field must specify generic type"); + } + final Type valueType = parameterizedType.getActualTypeArguments()[1]; + if (input instanceof Map inputMap) { + final Map castedInput = (Map)inputMap; + + final LinkedHashMap ret = new LinkedHashMap<>(); + + for (final Map.Entry entry : castedInput.entrySet()) { + ret.put(entry.getKey(), registry.deserialize(entry.getValue(), valueType)); + } + + return ret; + } + + throw new IllegalArgumentException("Not a map type: " + input.getClass()); + } + + @Override + public Map serialize(final TypeAdapterRegistry registry, final Map value, final Type type) { + final LinkedHashMap ret = new LinkedHashMap<>(); + + final Type valueType = type instanceof ParameterizedType parameterizedType ? parameterizedType.getActualTypeArguments()[1] : null; + + for (final Map.Entry entry : value.entrySet()) { + ret.put(entry.getKey(), registry.serialize(entry.getValue(), valueType)); + } + + return ret; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/primitive/BooleanTypeAdapter.java b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/primitive/BooleanTypeAdapter.java new file mode 100644 index 00000000..2b1cb8ff --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/primitive/BooleanTypeAdapter.java @@ -0,0 +1,33 @@ +package ca.spottedleaf.moonrise.common.config.adapter.primitive; + +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapterRegistry; +import java.lang.reflect.Type; + +public final class BooleanTypeAdapter extends TypeAdapter { + + public static final BooleanTypeAdapter INSTANCE = new BooleanTypeAdapter(); + + @Override + public Boolean deserialize(final TypeAdapterRegistry registry, final Object input, final Type type) { + if (input instanceof Boolean ret) { + return ret; + } + if (input instanceof String str) { + if (str.equalsIgnoreCase("false")) { + return Boolean.FALSE; + } + if (str.equalsIgnoreCase("true")) { + return Boolean.TRUE; + } + throw new IllegalArgumentException("Not a boolean: " + str); + } + + throw new IllegalArgumentException("Not a boolean type: " + input.getClass()); + } + + @Override + public Boolean serialize(final TypeAdapterRegistry registry, final Boolean value, final Type type) { + return value; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/primitive/ByteTypeAdapter.java b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/primitive/ByteTypeAdapter.java new file mode 100644 index 00000000..1f395690 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/primitive/ByteTypeAdapter.java @@ -0,0 +1,36 @@ +package ca.spottedleaf.moonrise.common.config.adapter.primitive; + +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapterRegistry; +import java.lang.reflect.Type; +import java.math.BigInteger; + +public final class ByteTypeAdapter extends TypeAdapter { + + public static final ByteTypeAdapter INSTANCE = new ByteTypeAdapter(); + + private static Byte cast(final Object original, final long value) { + if (value < (long)Byte.MIN_VALUE || value > (long)Byte.MAX_VALUE) { + throw new IllegalArgumentException("Byte value is out of range: " + original.toString()); + } + return Byte.valueOf((byte)value); + } + + @Override + public Byte deserialize(final TypeAdapterRegistry registry, final Object input, final Type type) { + if (input instanceof Number number) { + // note: silently discard floating point significand + return cast(input, number instanceof BigInteger bigInteger ? bigInteger.longValueExact() : number.longValue()); + } + if (input instanceof String string) { + return cast(input, (long)Double.parseDouble(string)); + } + + throw new IllegalArgumentException("Not a byte type: " + input.getClass()); + } + + @Override + public Byte serialize(final TypeAdapterRegistry registry, final Byte value, final Type type) { + return value; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/primitive/DoubleTypeAdapter.java b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/primitive/DoubleTypeAdapter.java new file mode 100644 index 00000000..00b5db66 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/primitive/DoubleTypeAdapter.java @@ -0,0 +1,27 @@ +package ca.spottedleaf.moonrise.common.config.adapter.primitive; + +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapterRegistry; +import java.lang.reflect.Type; + +public final class DoubleTypeAdapter extends TypeAdapter { + + public static final DoubleTypeAdapter INSTANCE = new DoubleTypeAdapter(); + + @Override + public Double deserialize(final TypeAdapterRegistry registry, final Object input, final Type type) { + if (input instanceof Number number) { + return Double.valueOf(number.doubleValue()); + } + if (input instanceof String string) { + return Double.valueOf(Double.parseDouble(string)); + } + + throw new IllegalArgumentException("Not a byte type: " + input.getClass()); + } + + @Override + public Double serialize(final TypeAdapterRegistry registry, final Double value, final Type type) { + return value; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/primitive/FloatTypeAdapter.java b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/primitive/FloatTypeAdapter.java new file mode 100644 index 00000000..80e4f3c6 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/primitive/FloatTypeAdapter.java @@ -0,0 +1,35 @@ +package ca.spottedleaf.moonrise.common.config.adapter.primitive; + +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapterRegistry; +import java.lang.reflect.Type; + +public final class FloatTypeAdapter extends TypeAdapter { + + public static final FloatTypeAdapter INSTANCE = new FloatTypeAdapter(); + + private static Float cast(final Object original, final double value) { + if (value < -(double)Float.MAX_VALUE || value > (double)Float.MAX_VALUE) { + throw new IllegalArgumentException("Byte value is out of range: " + original.toString()); + } + // note: silently ignore precision loss + return Float.valueOf((float)value); + } + + @Override + public Float deserialize(final TypeAdapterRegistry registry, final Object input, final Type type) { + if (input instanceof Number number) { + return cast(input, number.doubleValue()); + } + if (input instanceof String string) { + return cast(input, Double.parseDouble(string)); + } + + throw new IllegalArgumentException("Not a byte type: " + input.getClass()); + } + + @Override + public Float serialize(final TypeAdapterRegistry registry, final Float value, final Type type) { + return value; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/primitive/IntegerTypeAdapter.java b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/primitive/IntegerTypeAdapter.java new file mode 100644 index 00000000..7e3ffc25 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/primitive/IntegerTypeAdapter.java @@ -0,0 +1,36 @@ +package ca.spottedleaf.moonrise.common.config.adapter.primitive; + +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapterRegistry; +import java.lang.reflect.Type; +import java.math.BigInteger; + +public final class IntegerTypeAdapter extends TypeAdapter { + + public static final IntegerTypeAdapter INSTANCE = new IntegerTypeAdapter(); + + private static Integer cast(final Object original, final long value) { + if (value < (long)Integer.MIN_VALUE || value > (long)Integer.MAX_VALUE) { + throw new IllegalArgumentException("Integer value is out of range: " + original.toString()); + } + return Integer.valueOf((int)value); + } + + @Override + public Integer deserialize(final TypeAdapterRegistry registry, final Object input, final Type type) { + if (input instanceof Number number) { + // note: silently discard floating point significand + return cast(input, number instanceof BigInteger bigInteger ? bigInteger.longValueExact() : number.longValue()); + } + if (input instanceof String string) { + return cast(input, (long)Double.parseDouble(string)); + } + + throw new IllegalArgumentException("Not an integer type: " + input.getClass()); + } + + @Override + public Integer serialize(final TypeAdapterRegistry registry, final Integer value, final Type type) { + return value; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/primitive/LongTypeAdapter.java b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/primitive/LongTypeAdapter.java new file mode 100644 index 00000000..78e3d5a3 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/primitive/LongTypeAdapter.java @@ -0,0 +1,33 @@ +package ca.spottedleaf.moonrise.common.config.adapter.primitive; + +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapterRegistry; +import java.lang.reflect.Type; +import java.math.BigInteger; + +public final class LongTypeAdapter extends TypeAdapter { + + public static final LongTypeAdapter INSTANCE = new LongTypeAdapter(); + + @Override + public Long deserialize(final TypeAdapterRegistry registry, final Object input, final Type type) { + if (input instanceof Number number) { + // note: silently discard floating point significand + return number instanceof BigInteger bigInteger ? bigInteger.longValueExact() : number.longValue(); + } + if (input instanceof String string) { + try { + return Long.valueOf(Long.parseLong(string)); + } catch (final NumberFormatException ex) { + return Long.valueOf((long)Double.parseDouble(string)); + } + } + + throw new IllegalArgumentException("Not a long type: " + input.getClass()); + } + + @Override + public Long serialize(final TypeAdapterRegistry registry, final Long value, final Type type) { + return value; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/primitive/ShortTypeAdapter.java b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/primitive/ShortTypeAdapter.java new file mode 100644 index 00000000..43c3c127 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/primitive/ShortTypeAdapter.java @@ -0,0 +1,36 @@ +package ca.spottedleaf.moonrise.common.config.adapter.primitive; + +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapterRegistry; +import java.lang.reflect.Type; +import java.math.BigInteger; + +public final class ShortTypeAdapter extends TypeAdapter { + + public static final ShortTypeAdapter INSTANCE = new ShortTypeAdapter(); + + private static Short cast(final Object original, final long value) { + if (value < (long)Short.MIN_VALUE || value > (long)Short.MAX_VALUE) { + throw new IllegalArgumentException("Short value is out of range: " + original.toString()); + } + return Short.valueOf((short)value); + } + + @Override + public Short deserialize(final TypeAdapterRegistry registry, final Object input, final Type type) { + if (input instanceof Number number) { + // note: silently discard floating point significand + return cast(input, number instanceof BigInteger bigInteger ? bigInteger.longValueExact() : number.longValue()); + } + if (input instanceof String string) { + return cast(input, (long)Double.parseDouble(string)); + } + + throw new IllegalArgumentException("Not a short type: " + input.getClass()); + } + + @Override + public Short serialize(final TypeAdapterRegistry registry, final Short value, final Type type) { + return value; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/primitive/StringTypeAdapter.java b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/primitive/StringTypeAdapter.java new file mode 100644 index 00000000..32979f4c --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/primitive/StringTypeAdapter.java @@ -0,0 +1,29 @@ +package ca.spottedleaf.moonrise.common.config.adapter.primitive; + +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapterRegistry; +import java.lang.reflect.Type; + +public final class StringTypeAdapter extends TypeAdapter { + + public static final StringTypeAdapter INSTANCE = new StringTypeAdapter(); + + @Override + public String deserialize(final TypeAdapterRegistry registry, final Object input, final Type type) { + if (input instanceof Boolean bool) { + return String.valueOf(bool.booleanValue()); + } + if (input instanceof Number number) { + return number.toString(); + } + if (input instanceof String string) { + return string; + } + throw new IllegalArgumentException("Not a string type: " + input.getClass()); + } + + @Override + public String serialize(final TypeAdapterRegistry registry, final String value, final Type type) { + return value; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/type/BigDecimalTypeAdapter.java b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/type/BigDecimalTypeAdapter.java new file mode 100644 index 00000000..df9e6509 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/type/BigDecimalTypeAdapter.java @@ -0,0 +1,30 @@ +package ca.spottedleaf.moonrise.common.config.adapter.type; + +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapterRegistry; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.math.BigInteger; + +public final class BigDecimalTypeAdapter extends TypeAdapter { + + public static final BigDecimalTypeAdapter INSTANCE = new BigDecimalTypeAdapter(); + + @Override + public BigDecimal deserialize(final TypeAdapterRegistry registry, final Object input, final Type type) { + if (input instanceof Number number) { + // safest to catch all number impls is to use toString + return new BigDecimal(number.toString()); + } + if (input instanceof String string) { + return new BigDecimal(string); + } + + throw new IllegalArgumentException("Not an BigDecimal type: " + input.getClass()); + } + + @Override + public String serialize(final TypeAdapterRegistry registry, final BigDecimal value, final Type type) { + return value.toString(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/type/BigIntegerTypeAdapter.java b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/type/BigIntegerTypeAdapter.java new file mode 100644 index 00000000..9dbfc718 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/type/BigIntegerTypeAdapter.java @@ -0,0 +1,37 @@ +package ca.spottedleaf.moonrise.common.config.adapter.type; + +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapterRegistry; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.math.BigInteger; + +public final class BigIntegerTypeAdapter extends TypeAdapter { + + public static final BigIntegerTypeAdapter INSTANCE = new BigIntegerTypeAdapter(); + + @Override + public BigInteger deserialize(final TypeAdapterRegistry registry, final Object input, final Type type) { + if (input instanceof Number number) { + if (number instanceof BigInteger bigInteger) { + return bigInteger; + } + // note: silently discard floating point significand + if (number instanceof BigDecimal bigDecimal) { + return bigDecimal.toBigInteger(); + } + + return BigInteger.valueOf(number.longValue()); + } + if (input instanceof String string) { + return new BigDecimal(string).toBigInteger(); + } + + throw new IllegalArgumentException("Not an BigInteger type: " + input.getClass()); + } + + @Override + public String serialize(final TypeAdapterRegistry registry, final BigInteger value, final Type type) { + return value.toString(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/type/DurationTypeAdapter.java b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/type/DurationTypeAdapter.java new file mode 100644 index 00000000..6effd1a4 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/config/adapter/type/DurationTypeAdapter.java @@ -0,0 +1,24 @@ +package ca.spottedleaf.moonrise.common.config.adapter.type; + +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapter; +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapterRegistry; +import ca.spottedleaf.moonrise.common.config.type.Duration; +import java.lang.reflect.Type; + +public final class DurationTypeAdapter extends TypeAdapter { + + public static final DurationTypeAdapter INSTANCE = new DurationTypeAdapter(); + + @Override + public Duration deserialize(final TypeAdapterRegistry registry, final Object input, final Type type) { + if (!(input instanceof String string)) { + throw new IllegalArgumentException("Not a string: " + input.getClass()); + } + return Duration.parse(string); + } + + @Override + public String serialize(final TypeAdapterRegistry registry, final Duration value, final Type type) { + return value.toString(); + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/config/annotation/Adaptable.java b/src/main/java/ca/spottedleaf/moonrise/common/config/annotation/Adaptable.java new file mode 100644 index 00000000..af87382c --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/config/annotation/Adaptable.java @@ -0,0 +1,15 @@ +package ca.spottedleaf.moonrise.common.config.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation used on a class to indicate that its type adapter may automatically be generated. The class must have + * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Adaptable { +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/config/annotation/Serializable.java b/src/main/java/ca/spottedleaf/moonrise/common/config/annotation/Serializable.java new file mode 100644 index 00000000..37ddecdc --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/config/annotation/Serializable.java @@ -0,0 +1,40 @@ +package ca.spottedleaf.moonrise.common.config.annotation; + +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapter; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.function.Function; + +/** + * Annotation indicating that a field should be deserialized or serialized from the config. + * By default, this annotation is assumed + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Serializable { + + /** + * Indicates whether this field is required to be present in the config. If the field is not present, + * and {@code required = true}, then an exception will be thrown during deserialization. If {@code required = false} + * and the field is not present, then the field value will remain unmodified. + */ + public boolean required() default false; + + /** + * The comment to apply before the element when serializing. + */ + public String comment() default ""; + + /** + * Adapter override class. The class must have a public no-args constructor. + */ + public Class adapter() default TypeAdapter.class; + + /** + * Whether to serialize the value to the config. + */ + public boolean serialize() default true; + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/config/config/YamlConfig.java b/src/main/java/ca/spottedleaf/moonrise/common/config/config/YamlConfig.java new file mode 100644 index 00000000..3e6b975c --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/config/config/YamlConfig.java @@ -0,0 +1,136 @@ +package ca.spottedleaf.moonrise.common.config.config; + +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapterRegistry; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.comments.CommentLine; +import org.yaml.snakeyaml.comments.CommentType; +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.representer.Represent; +import org.yaml.snakeyaml.representer.Representer; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +public final class YamlConfig { + + public final TypeAdapterRegistry typeAdapters = new TypeAdapterRegistry(); + + private final Class clazz; + + public T config; + + private final Yaml yaml; + + public YamlConfig(final Class clazz, final T dfl) throws Exception { + this.clazz = clazz; + this.config = dfl; + this.typeAdapters.makeAdapter(clazz); + + final LoaderOptions loaderOptions = new LoaderOptions(); + loaderOptions.setProcessComments(true); + + final DumperOptions dumperOptions = new DumperOptions(); + dumperOptions.setProcessComments(true); + dumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + + this.yaml = new Yaml(new YamlConstructor(loaderOptions), new YamlRepresenter(dumperOptions), dumperOptions, loaderOptions); + } + + public void load(final File file) throws IOException { + try (final InputStream is = new BufferedInputStream(new FileInputStream(file))) { + this.load(is); + } + } + + public void load(final InputStream is) throws IOException { + final Object serialized = this.yaml.load(new InputStreamReader(is, StandardCharsets.UTF_8)); + + this.config = (T)this.typeAdapters.deserialize(serialized, this.clazz); + } + + public void save(final File file) throws IOException { + if (file.isDirectory()) { + throw new IOException("File is a directory"); + } + + final File tmp = new File(file.getParentFile(), file.getName() + ".tmp"); + tmp.delete(); + tmp.createNewFile(); + try { + try (final OutputStream os = new BufferedOutputStream(new FileOutputStream(tmp))) { + this.save(os); + } + + try { + Files.move(tmp.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (final AtomicMoveNotSupportedException ex) { + Files.move(tmp.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + } finally { + tmp.delete(); + } + } + + public void save(final OutputStream os) throws IOException { + os.write(this.saveToString().getBytes(StandardCharsets.UTF_8)); + } + + public String saveToString() { + return this.yaml.dump(this.typeAdapters.serialize(this.config, this.clazz)); + } + + private static final class YamlConstructor extends Constructor { + + public YamlConstructor(final LoaderOptions loadingConfig) { + super(loadingConfig); + } + } + + private static final class YamlRepresenter extends Representer { + + public YamlRepresenter(final DumperOptions options) { + super(options); + + this.representers.put(TypeAdapterRegistry.CommentedData.class, new CommentedDataRepresenter()); + + } + + private final class CommentedDataRepresenter implements Represent { + + @Override + public Node representData(final Object data0) { + final TypeAdapterRegistry.CommentedData commentedData = (TypeAdapterRegistry.CommentedData)data0; + + final Node node = YamlRepresenter.this.representData(commentedData.data); + + final List comments = new ArrayList<>(); + + for (final String line : commentedData.comment.split("\n")) { + comments.add(new CommentLine(null, null, " ".concat(line.trim()), CommentType.BLOCK)); + } + + node.setBlockComments(comments); + + return node; + } + } + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/config/type/Duration.java b/src/main/java/ca/spottedleaf/moonrise/common/config/type/Duration.java new file mode 100644 index 00000000..5c7cecdb --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/config/type/Duration.java @@ -0,0 +1,76 @@ +package ca.spottedleaf.moonrise.common.config.type; + +import java.math.BigDecimal; + +public final class Duration { + + private final String string; + private final long timeNS; + + private Duration(final String string, final long timeNS) { + this.string = string; + this.timeNS = timeNS; + } + + public static Duration parse(final String value) { + if (value.length() < 2) { + throw new IllegalArgumentException("Invalid duration: " + value); + } + + final char last = value.charAt(value.length() - 1); + + final long multiplier; + + switch (last) { + case 's': { + multiplier = (1000L * 1000L * 1000L) * 1L; + break; + } + case 't': { + multiplier = (1000L * 1000L * 1000L) / 20L; + break; + } + case 'm': { + multiplier = (1000L * 1000L * 1000L) * 60L; + break; + } + case 'h': { + multiplier = (1000L * 1000L * 1000L) * 60L * 60L; + break; + } + case 'd': { + multiplier = (1000L * 1000L * 1000L) * 24L * 60L * 60L; + break; + } + default: { + throw new IllegalArgumentException("Duration must end with one of: [s, t, m, h, d]"); + } + } + + final BigDecimal parsed = new BigDecimal(value.substring(0, value.length() - 1)) + .multiply(new BigDecimal(multiplier)); + + return new Duration(value, parsed.toBigInteger().longValueExact()); + } + + public long getTimeNS() { + return this.timeNS; + } + + public long getTimeMS() { + return this.timeNS / (1000L * 1000L); + } + + public long getTimeS() { + return this.timeNS / (1000L * 1000L * 1000L); + } + + public long getTimeTicks() { + return this.timeNS / ((1000L * 1000L * 1000L) / (20L)); + } + + @Override + public String toString() { + return this.string; + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java index e5bee7b1..41d048bf 100644 --- a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java +++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java @@ -1,14 +1,57 @@ package ca.spottedleaf.moonrise.common.util; import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool; -import ca.spottedleaf.moonrise.common.config.PlaceholderConfig; +import ca.spottedleaf.moonrise.common.config.MoonriseConfig; +import ca.spottedleaf.moonrise.common.config.adapter.TypeAdapterRegistry; +import ca.spottedleaf.moonrise.common.config.config.YamlConfig; import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.File; public final class MoonriseCommon { - private static final Logger LOGGER = LoggerFactory.getLogger(ChunkTaskScheduler.class); + private static final Logger LOGGER = LoggerFactory.getLogger(MoonriseCommon.class); + + private static final File CONFIG_FILE = new File("moonrise.yaml"); + private static final YamlConfig CONFIG; + static { + try { + CONFIG = new YamlConfig<>(MoonriseConfig.class, new MoonriseConfig()); + } catch (final Exception ex) { + throw new RuntimeException(ex); + } + } + + static { + reloadConfig(); + } + + public static MoonriseConfig getConfig() { + return CONFIG.config; + } + + public static void reloadConfig() { + if (CONFIG_FILE.exists()) { + try { + CONFIG.load(CONFIG_FILE); + } catch (final Exception ex) { + LOGGER.error("Failed to load configuration, using defaults", ex); + return; + } + } + + // write back any changes, or create if needed + saveConfig(); + } + + public static void saveConfig() { + try { + CONFIG.save(CONFIG_FILE); + } catch (final Exception ex) { + LOGGER.error("Failed to save configuration", ex); + } + } public static final PrioritisedThreadPool WORKER_POOL; public static final int WORKER_THREADS; @@ -21,7 +64,7 @@ public final class MoonriseCommon { } defaultWorkerThreads = Integer.getInteger("Moonrise.WorkerThreadCount", Integer.valueOf(defaultWorkerThreads)); - int workerThreads = PlaceholderConfig.workerThreads; + int workerThreads = MoonriseCommon.getConfig().workerThreads; if (workerThreads < 0) { workerThreads = defaultWorkerThreads; @@ -44,5 +87,4 @@ public void uncaughtException(final Thread thread, final Throwable throwable) { } private MoonriseCommon() {} - } diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java index 8b32c4fc..8a4d35b0 100644 --- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java @@ -2,10 +2,10 @@ import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; -import ca.spottedleaf.moonrise.common.config.PlaceholderConfig; import ca.spottedleaf.moonrise.common.misc.AllocatingRateLimiter; import ca.spottedleaf.moonrise.common.misc.SingleUserAreaMap; import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.MoonriseCommon; import ca.spottedleaf.moonrise.common.util.TickThread; import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel; import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; @@ -46,7 +46,7 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; -public class RegionizedPlayerChunkLoader { +public final class RegionizedPlayerChunkLoader { public static final TicketType PLAYER_TICKET = TicketType.create("chunk_system:player_ticket", Long::compareTo); public static final TicketType PLAYER_TICKET_DELAYED = TicketType.create("chunk_system:player_ticket_delayed", Long::compareTo, 5 * 20); @@ -512,7 +512,7 @@ private static int getSendViewDistance(final int loadViewDistance, final int cli final int playerSendViewDistance, final int worldSendViewDistance) { return Math.min( loadViewDistance - 1, - playerSendViewDistance < 0 ? (!PlaceholderConfig.chunkLoadingAdvanced$autoConfigSendDistance || clientViewDistance < 0 ? (worldSendViewDistance < 0 ? (loadViewDistance - 1) : worldSendViewDistance) : clientViewDistance + 1) : playerSendViewDistance + playerSendViewDistance < 0 ? (!MoonriseCommon.getConfig().chunkLoading.advanced.autoConfigSendDistance || clientViewDistance < 0 ? (worldSendViewDistance < 0 ? (loadViewDistance - 1) : worldSendViewDistance) : clientViewDistance + 1) : playerSendViewDistance ); } @@ -537,26 +537,26 @@ private boolean canPlayerGenerateChunks() { } private double getMaxChunkLoadRate() { - final double configRate = PlaceholderConfig.chunkLoadingBasic$playerMaxChunkLoadRate; + final double configRate = MoonriseCommon.getConfig().chunkLoading.basic.playerMaxLoadRate; return configRate < 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate); } private double getMaxChunkGenRate() { - final double configRate = PlaceholderConfig.chunkLoadingBasic$playerMaxChunkGenerateRate; + final double configRate = MoonriseCommon.getConfig().chunkLoading.basic.playerMaxGenRate; return configRate < 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate); } private double getMaxChunkSendRate() { - final double configRate = PlaceholderConfig.chunkLoadingBasic$playerMaxChunkSendRate; + final double configRate = MoonriseCommon.getConfig().chunkLoading.basic.playerMaxSendRate; return configRate < 0.0 || configRate > (double)MAX_RATE ? (double)MAX_RATE : Math.max(1.0, configRate); } private long getMaxChunkLoads() { final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L); - long configLimit = PlaceholderConfig.chunkLoadingAdvanced$playerMaxConcurrentChunkLoads; + long configLimit = MoonriseCommon.getConfig().chunkLoading.advanced.playerMaxConcurrentChunkLoads; if (configLimit == 0L) { // by default, only allow 1/5th of the chunks in the view distance to be concurrently active configLimit = Math.max(5L, radiusChunks / 5L); @@ -570,7 +570,7 @@ private long getMaxChunkLoads() { private long getMaxChunkGenerates() { final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L); - long configLimit = PlaceholderConfig.chunkLoadingAdvanced$playerMaxConcurrentChunkGenerates; + long configLimit = MoonriseCommon.getConfig().chunkLoading.advanced.playerMaxConcurrentChunkGenerates; if (configLimit == 0L) { // by default, only allow 1/5th of the chunks in the view distance to be concurrently active configLimit = Math.max(5L, radiusChunks / 5L); diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java index a111c1ee..3d0658ab 100644 --- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java @@ -3,8 +3,8 @@ import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; -import ca.spottedleaf.moonrise.common.config.PlaceholderConfig; import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.MoonriseCommon; import ca.spottedleaf.moonrise.common.util.TickThread; import ca.spottedleaf.moonrise.common.util.WorldUtil; import ca.spottedleaf.moonrise.patches.chunk_system.ChunkSystem; @@ -230,8 +230,9 @@ void ensureInAutosave(final NewChunkHolder holder) { public void autoSave() { final List reschedule = new ArrayList<>(); final long currentTick = this.currentTick; - final long maxSaveTime = currentTick - (long)PlaceholderConfig.autoSaveInterval; - for (int autoSaved = 0; autoSaved < (long)PlaceholderConfig.maxAutoSaveChunksPerTick && !this.autoSaveQueue.isEmpty();) { + final long maxSaveTime = currentTick - Math.max(1L, MoonriseCommon.getConfig().chunkSaving.autoSaveInterval.getTimeTicks()); + final int maxToSave = MoonriseCommon.getConfig().chunkSaving.maxAutoSaveChunksPerTick; + for (int autoSaved = 0; autoSaved < maxToSave && !this.autoSaveQueue.isEmpty();) { final NewChunkHolder holder = this.autoSaveQueue.first(); if (holder.lastAutoSave > maxSaveTime) { diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java index 8fabca97..be3485c2 100644 --- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java @@ -5,7 +5,6 @@ import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadedTaskQueue; import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock; import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; -import ca.spottedleaf.moonrise.common.config.PlaceholderConfig; import ca.spottedleaf.moonrise.common.util.CoordinateUtils; import ca.spottedleaf.moonrise.common.util.MoonriseCommon; import ca.spottedleaf.moonrise.common.util.TickThread; @@ -51,6 +50,7 @@ public final class ChunkTaskScheduler { static int newChunkSystemIOThreads; static int newChunkSystemGenParallelism; + static int newChunkSystemGenPopulationParallelism; static int newChunkSystemLoadParallelism; private static boolean initialised = false; @@ -60,29 +60,17 @@ public static void init() { return; } initialised = true; - newChunkSystemIOThreads = PlaceholderConfig.chunkSystemIOThreads; + newChunkSystemIOThreads = MoonriseCommon.getConfig().chunkSystem.ioThreads; if (newChunkSystemIOThreads < 0) { newChunkSystemIOThreads = 1; } else { newChunkSystemIOThreads = Math.max(1, newChunkSystemIOThreads); } - String newChunkSystemGenParallelism = PlaceholderConfig.chunkSystemGenParallelism; - if (newChunkSystemGenParallelism.equalsIgnoreCase("default")) { - newChunkSystemGenParallelism = "true"; - } - boolean useParallelGen; - if (newChunkSystemGenParallelism.equalsIgnoreCase("on") || newChunkSystemGenParallelism.equalsIgnoreCase("enabled") - || newChunkSystemGenParallelism.equalsIgnoreCase("true")) { - useParallelGen = true; - } else if (newChunkSystemGenParallelism.equalsIgnoreCase("off") || newChunkSystemGenParallelism.equalsIgnoreCase("disabled") - || newChunkSystemGenParallelism.equalsIgnoreCase("false")) { - useParallelGen = false; - } else { - throw new IllegalStateException("Invalid option for gen-parallelism: must be one of [on, off, enabled, disabled, true, false, default]"); - } + boolean useParallelGen = MoonriseCommon.getConfig().chunkSystem.populationGenParallelism; - ChunkTaskScheduler.newChunkSystemGenParallelism = useParallelGen ? MoonriseCommon.WORKER_THREADS : 1; + ChunkTaskScheduler.newChunkSystemGenParallelism = MoonriseCommon.WORKER_THREADS; + ChunkTaskScheduler.newChunkSystemGenPopulationParallelism = useParallelGen ? MoonriseCommon.WORKER_THREADS : 1; ChunkTaskScheduler.newChunkSystemLoadParallelism = MoonriseCommon.WORKER_THREADS; RegionFileIOThread.init(newChunkSystemIOThreads); @@ -277,10 +265,9 @@ public ChunkTaskScheduler(final ServerLevel world, final PrioritisedThreadPool w final String worldName = WorldUtil.getWorldName(world); this.parallelGenExecutor = workers.createExecutor("Chunk parallel generation executor for world '" + worldName + "'", 1, Math.max(1, newChunkSystemGenParallelism)); - this.radiusAwareGenExecutor = - newChunkSystemGenParallelism <= 1 ? this.parallelGenExecutor : workers.createExecutor("Chunk radius aware generator for world '" + worldName + "'", 1, newChunkSystemGenParallelism); + this.radiusAwareGenExecutor = workers.createExecutor("Chunk radius aware generator for world '" + worldName + "'", 1, Math.max(1, newChunkSystemGenPopulationParallelism)); this.loadExecutor = workers.createExecutor("Chunk load executor for world '" + worldName + "'", 1, newChunkSystemLoadParallelism); - this.radiusAwareScheduler = new RadiusAwarePrioritisedExecutor(this.radiusAwareGenExecutor, Math.max(1, newChunkSystemGenParallelism)); + this.radiusAwareScheduler = new RadiusAwarePrioritisedExecutor(this.radiusAwareGenExecutor, Math.max(1, newChunkSystemGenPopulationParallelism)); this.chunkHolderManager = new ChunkHolderManager(world, this); }