diff --git a/README.md b/README.md index 5b01f3b9..6b9280e2 100644 --- a/README.md +++ b/README.md @@ -18,3 +18,7 @@ #### [HOW TO ADD MODULE](./ADD-MODULE.md) + + +## Another +- Config API part based [`quilt-config`](https://github.com/QuiltMC/quilt-config) diff --git a/command-neo/src/main/resources/META-INF/neoforge.mods.toml b/command-neo/src/main/resources/META-INF/neoforge.mods.toml index 6b937418..7d72a5ff 100644 --- a/command-neo/src/main/resources/META-INF/neoforge.mods.toml +++ b/command-neo/src/main/resources/META-INF/neoforge.mods.toml @@ -14,14 +14,14 @@ logoFile = "icon.png" authors = "Kessoku Tea Time" displayURL = "https://modrinth.com/mod/kessoku-lib" -[[dependencies.kessoku-event-base]] +[[dependencies.kessoku_command]] modId = "neoforge" type = "required" versionRange = "[21.0,)" ordering = "NONE" side = "BOTH" -[[dependencies.kessoku-event-base]] +[[dependencies.kessoku_command]] modId = "minecraft" type = "required" versionRange = "[1.21,)" diff --git a/config-common/build.gradle b/config-common/build.gradle new file mode 100644 index 00000000..5b91867e --- /dev/null +++ b/config-common/build.gradle @@ -0,0 +1,16 @@ +apply from: rootProject.file("gradle/scripts/kessokulib-common.gradle") + +group = "band.kessoku.lib.config" +base.archivesName = rootProject.name + "-config" + +repositories { + maven { url = "https://maven.quiltmc.org/repository/release/" } +} + +dependencies { + moduleImplementation(project(":base-common")) + moduleImplementation(project(":platform-common")) + + implementation(libs.bundles.night.config) + implementation(libs.bundles.quilt.config) +} \ No newline at end of file diff --git a/config-common/src/main/java/band/kessoku/lib/config/KessokuConfig.java b/config-common/src/main/java/band/kessoku/lib/config/KessokuConfig.java new file mode 100644 index 00000000..420a822b --- /dev/null +++ b/config-common/src/main/java/band/kessoku/lib/config/KessokuConfig.java @@ -0,0 +1,9 @@ +package band.kessoku.lib.config; + +import org.slf4j.Marker; +import org.slf4j.MarkerFactory; + +public class KessokuConfig { + public static final String MOD_ID = "kessoku_config"; + public static final Marker MARKER = MarkerFactory.getMarker("[KessokuConfig]"); +} diff --git a/config-common/src/main/java/band/kessoku/lib/config/api/ModConfigHelper.java b/config-common/src/main/java/band/kessoku/lib/config/api/ModConfigHelper.java new file mode 100644 index 00000000..cb8c0c94 --- /dev/null +++ b/config-common/src/main/java/band/kessoku/lib/config/api/ModConfigHelper.java @@ -0,0 +1,157 @@ +package band.kessoku.lib.config.api; + +import band.kessoku.lib.config.impl.ModConfigHelperImpl; +import org.quiltmc.config.api.Config; +import org.quiltmc.config.api.ReflectiveConfig; +import org.quiltmc.config.impl.ConfigImpl; +import org.quiltmc.config.implementor_api.ConfigFactory; + +import java.nio.file.Path; +import java.nio.file.Paths; + +public final class ModConfigHelper { + /** + * Creates and registers a config file + * + * @param family the mod owning the resulting config file + * @param id the configs id + * @param path additional path elements to include as part of this configs file, e.g. + * if the path is empty, the config file might be ".minecraft/config/example_mod/id.toml" + * if the path is "client/gui", the config file might be ".minecraft/config/example_mod/client/gui/id.toml" + * @param creators any number of {@link Config.Creator}s that can be used to configure the resulting config + */ + public static Config create(String family, String id, Path path, Config.Creator... creators) { + return ConfigImpl.create(ModConfigHelperImpl.getConfigEnvironment(), family, id, path, creators); + } + + /** + * Creates and registers a config file + * + * @param family the mod owning the resulting config file + * @param id the configs id + * @param creators any number of {@link Config.Creator}s that can be used to configure the resulting config + */ + public static Config create(String family, String id, Config.Creator... creators) { + return create(family, id, Paths.get(""), creators); + } + + /** + * Creates and registers a config with a class that contains its WrappedValues as fields. + * + * @param family the mod owning the resulting config file + * @param id the config's id + * @param path additional path elements to include as part of this configs file, e.g. + * if the path is empty, the config file might be ".minecraft/config/example_mod/id.toml" + * if the path is "client/gui", the config file might be ".minecraft/config/example_mod/client/gui/id.toml" + * @param before a {@link Config.Creator} that can be used to configure the resulting config further + * @param configCreatorClass a class as described above + * @param after a {@link Config.Creator} that can be used to configure the resulting config further + * @return a {@link ReflectiveConfig } + */ + public static C create(String family, String id, Path path, Config.Creator before, Class configCreatorClass, Config.Creator after) { + return ConfigFactory.create(ModConfigHelperImpl.getConfigEnvironment(), family, id, path, before, configCreatorClass, after); + } + + /** + * Creates and registers a config with a class that contains its WrappedValues as fields. + * + * @param family the mod owning the resulting config file + * @param id the config's id + * @param path additional path elements to include as part of this configs file, e.g. + * if the path is empty, the config file might be ".minecraft/config/example_mod/id.toml" + * if the path is "client/gui", the config file might be ".minecraft/config/example_mod/client/gui/id.toml" + * @param before a {@link Config.Creator} that can be used to configure the resulting config further + * @param configCreatorClass a class as described above + * @return a {@link ReflectiveConfig } + */ + public static C create(String family, String id, Path path, Config.Creator before, Class configCreatorClass) { + return create(family, id, path, before, configCreatorClass, builder -> {}); + } + + /** + * Creates and registers a config with a class that contains its WrappedValues as fields. + * + * @param family the mod owning the resulting config file + * @param id the configs id + * @param path additional path elements to include as part of this configs file, e.g. + * if the path is empty, the config file might be ".minecraft/config/example_mod/id.toml" + * if the path is "client/gui", the config file might be ".minecraft/config/example_mod/client/gui/id.toml" + * @param configCreatorClass a class as described above + * @param after a {@link Config.Creator} that can be used to configure the resulting config further + * @return a {@link ReflectiveConfig } + */ + public static C create(String family, String id, Path path, Class configCreatorClass, Config.Creator after) { + return create(family, id, path, builder -> {}, configCreatorClass, after); + } + + /** + * Creates and registers a config with a class that contains its WrappedValues as fields. + * + * @param family the mod owning the resulting config file + * @param id the config's id + * @param path additional path elements to include as part of this configs file, e.g. + * if the path is empty, the config file might be ".minecraft/config/example_mod/id.toml" + * if the path is "client/gui", the config file might be ".minecraft/config/example_mod/client/gui/id.toml" + * @param configCreatorClass a class as described above + * @return a {@link ReflectiveConfig } + */ + public static C create(String family, String id, Path path, Class configCreatorClass) { + return create(family, id, path, builder -> {}, configCreatorClass, builder -> {}); + } + + /** + * Creates and registers a config with a class that contains its WrappedValues as fields. + * + * @param family the mod owning the resulting config file + * @param id the config's id + * @param before a {@link Config.Creator} that can be used to configure the resulting config further + * @param configCreatorClass a class as described above + * @param after a {@link Config.Creator} that can be used to configure the resulting config further + * @return a {@link ReflectiveConfig } + */ + public static C create(String family, String id, Config.Creator before, Class configCreatorClass, Config.Creator after) { + return create(family, id, Paths.get(""), before, configCreatorClass, after); + } + + /** + * Creates and registers a config with a class that contains its WrappedValues as fields. + * + * @param family the mod owning the resulting config file + * @param id the config's id + * @param before a {@link Config.Creator} that can be used to configure the resulting config further + * @param configCreatorClass a class as described above + * @return a {@link ReflectiveConfig } + */ + public static C create(String family, String id, Config.Creator before, Class configCreatorClass) { + return create(family, id, Paths.get(""), before, configCreatorClass, builder -> {}); + } + + /** + * Creates and registers a config with a class that contains its WrappedValues as fields. + * + * @param family the mod owning the resulting config file + * @param id the config's id + * @param configCreatorClass a class as described above + * @param after a {@link Config.Creator} that can be used to configure the resulting config further + * @return a {@link ReflectiveConfig } + */ + public static C create(String family, String id, Class configCreatorClass, Config.Creator after) { + return create(family, id, Paths.get(""), builder -> {}, configCreatorClass, after); + } + + /** + * Creates and registers a config with a class that contains its WrappedValues as fields. + * + * @param family the mod owning the resulting config file + * @param id the config's id + * @param configCreatorClass a class as described above + * @return a {@link ReflectiveConfig } + */ + public static C create(String family, String id, Class configCreatorClass) { + return create(family, id, Paths.get(""), builder -> {}, configCreatorClass, builder -> {}); + } + + private ModConfigHelper() { + + } +} diff --git a/config-common/src/main/java/band/kessoku/lib/config/api/serializers/night/HoconSerializer.java b/config-common/src/main/java/band/kessoku/lib/config/api/serializers/night/HoconSerializer.java new file mode 100644 index 00000000..850443cb --- /dev/null +++ b/config-common/src/main/java/band/kessoku/lib/config/api/serializers/night/HoconSerializer.java @@ -0,0 +1,148 @@ +package band.kessoku.lib.config.api.serializers.night; + +import com.electronwill.nightconfig.core.CommentedConfig; +import com.electronwill.nightconfig.core.InMemoryCommentedFormat; +import com.electronwill.nightconfig.core.UnmodifiableCommentedConfig; +import com.electronwill.nightconfig.core.io.ConfigParser; +import com.electronwill.nightconfig.core.io.ConfigWriter; +import com.electronwill.nightconfig.hocon.HoconParser; +import com.electronwill.nightconfig.hocon.HoconWriter; +import org.quiltmc.config.api.Config; +import org.quiltmc.config.api.Constraint; +import org.quiltmc.config.api.MarshallingUtils; +import org.quiltmc.config.api.Serializer; +import org.quiltmc.config.api.annotations.Comment; +import org.quiltmc.config.api.values.*; +import org.quiltmc.config.impl.util.SerializerUtils; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.*; + +/** + * A default serializer that writes in the HOCON format. + * + * @implNote When passing entries to {@link com.electronwill.nightconfig.core.Config#add(String, Object)}, the string key is automatically split at each dot ({@code .}). + * This completely breaks TOML serialization, since we allow dots in keys, using either {@link org.quiltmc.config.api.annotations.SerializedName} or {@link ValueMap}, whose keys are not validated for certain characters. + * To get around this, use {@link com.electronwill.nightconfig.core.Config#add(List, Object)} via passing your key into {@link #toNightConfigSerializable(ValueKey)}. + */ +public final class HoconSerializer implements Serializer { + public static final HoconSerializer INSTANCE = new HoconSerializer(); + private final ConfigParser parser = new HoconParser(); + private final ConfigWriter writer = new HoconWriter(); + + private HoconSerializer() { + + } + + @Override + public String getFileExtension() { + return "hocon"; + } + + @Override + public void serialize(Config config, OutputStream to) { + this.writer.write(write(config, createCommentedConfig(), config.nodes()), to); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public void deserialize(Config config, InputStream from) { + CommentedConfig read = this.parser.parse(from); + + for (TrackedValue trackedValue : config.values()) { + List keyOptions = SerializerUtils.getPossibleKeys(config, trackedValue); + + for (ValueKey key : keyOptions) { + String stringKey = key.toString(); + + if (read.contains(stringKey)) { + ((TrackedValue) trackedValue).setValue(MarshallingUtils.coerce(read.get(stringKey), trackedValue.getDefaultValue(), (CommentedConfig c, MarshallingUtils.MapEntryConsumer entryConsumer) -> + c.entrySet().forEach(e -> entryConsumer.put(e.getKey(), e.getValue()))), false); + } + } + } + } + + private static List convertList(List list) { + List result = new ArrayList<>(list.size()); + + for (Object value : list) { + result.add(convertAny(value)); + } + + return result; + } + + private static UnmodifiableCommentedConfig convertMap(ValueMap map) { + CommentedConfig result = createCommentedConfig(); + + for (Map.Entry entry : map.entrySet()) { + List key = new ArrayList<>(); + key.add(entry.getKey()); + result.add(key, convertAny(entry.getValue())); + } + + return result; + } + + private static Object convertAny(Object value) { + if (value instanceof ValueMap) { + return convertMap((ValueMap) value); + } else if (value instanceof ValueList) { + return convertList((ValueList) value); + } else if (value instanceof ConfigSerializableObject) { + return convertAny(((ConfigSerializableObject) value).getRepresentation()); + } else { + return value; + } + } + + private static CommentedConfig write(Config config, CommentedConfig commentedConfig, Iterable nodes) { + for (ValueTreeNode node : nodes) { + List comments = new ArrayList<>(); + + if (node.hasMetadata(Comment.TYPE)) { + for (String string : node.metadata(Comment.TYPE)) { + comments.add(string); + } + } + + ValueKey key = SerializerUtils.getSerializedKey(config, node); + + if (node instanceof TrackedValue) { + TrackedValue value = (TrackedValue) node; + Object defaultValue = value.getDefaultValue(); + + SerializerUtils.createEnumOptionsComment(defaultValue).ifPresent(comments::add); + + for (Constraint constraint : value.constraints()) { + comments.add(constraint.getRepresentation()); + } + + Optional defaultValueComment = SerializerUtils.getDefaultValueString(defaultValue); + defaultValueComment.ifPresent(s -> comments.add("default: " + s)); + + commentedConfig.add(toNightConfigSerializable(key), convertAny(value.getRealValue())); + } else { + write(config, commentedConfig, ((ValueTreeNode.Section) node)); + } + + if (!comments.isEmpty()) { + commentedConfig.setComment(toNightConfigSerializable(key), " " + String.join("\n ", comments)); + } + } + + return commentedConfig; + } + + private static CommentedConfig createCommentedConfig() { + return InMemoryCommentedFormat.defaultInstance().createConfig(LinkedHashMap::new); + } + + private static List toNightConfigSerializable(ValueKey key) { + List listKey = new ArrayList<>(); + key.forEach(listKey::add); + return listKey; + } +} diff --git a/config-common/src/main/java/band/kessoku/lib/config/api/serializers/night/JsonSerializer.java b/config-common/src/main/java/band/kessoku/lib/config/api/serializers/night/JsonSerializer.java new file mode 100644 index 00000000..c110ae38 --- /dev/null +++ b/config-common/src/main/java/band/kessoku/lib/config/api/serializers/night/JsonSerializer.java @@ -0,0 +1,128 @@ +package band.kessoku.lib.config.api.serializers.night; + +import com.electronwill.nightconfig.core.*; +import com.electronwill.nightconfig.core.io.ConfigParser; +import com.electronwill.nightconfig.core.io.ConfigWriter; +import com.electronwill.nightconfig.json.FancyJsonWriter; +import com.electronwill.nightconfig.json.JsonParser; +import org.quiltmc.config.api.Config; +import org.quiltmc.config.api.MarshallingUtils; +import org.quiltmc.config.api.Serializer; +import org.quiltmc.config.api.values.ConfigSerializableObject; +import org.quiltmc.config.api.values.TrackedValue; +import org.quiltmc.config.api.values.ValueKey; +import org.quiltmc.config.api.values.ValueList; +import org.quiltmc.config.api.values.ValueMap; +import org.quiltmc.config.api.values.ValueTreeNode; +import org.quiltmc.config.impl.util.SerializerUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.*; + +/** + * A default serializer that writes in the JSON format. + * + * @implNote When passing entries to {@link com.electronwill.nightconfig.core.Config#add(String, Object)}, the string key is automatically split at each dot ({@code .}). + * This completely breaks TOML serialization, since we allow dots in keys, using either {@link org.quiltmc.config.api.annotations.SerializedName} or {@link ValueMap}, whose keys are not validated for certain characters. + * To get around this, use {@link com.electronwill.nightconfig.core.Config#add(List, Object)} via passing your key into {@link #toNightConfigSerializable(ValueKey)}. + */ +public final class JsonSerializer implements Serializer { + public static final JsonSerializer INSTANCE = new JsonSerializer(); + private final ConfigParser parser = new JsonParser(); + private final ConfigWriter writer = new FancyJsonWriter(); + + private JsonSerializer() { + + } + + @Override + public String getFileExtension() { + return "json"; + } + + @Override + public void serialize(Config config, OutputStream to) throws IOException { + this.writer.write(write(config, createUncommentedConfig(), config.nodes()), to); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public void deserialize(Config config, InputStream from) { + com.electronwill.nightconfig.core.Config read = this.parser.parse(from); + + for (TrackedValue trackedValue : config.values()) { + List keyOptions = SerializerUtils.getPossibleKeys(config, trackedValue); + + for (ValueKey key : keyOptions) { + String stringKey = key.toString(); + + if (read.contains(stringKey)) { + ((TrackedValue) trackedValue).setValue(MarshallingUtils.coerce(read.get(stringKey), trackedValue.getDefaultValue(), (com.electronwill.nightconfig.core.Config c, MarshallingUtils.MapEntryConsumer entryConsumer) -> + c.entrySet().forEach(e -> entryConsumer.put(e.getKey(), e.getValue()))), false); + } + } + } + } + + private static List convertList(List list) { + List result = new ArrayList<>(list.size()); + + for (Object value : list) { + result.add(convertAny(value)); + } + + return result; + } + + private static UnmodifiableConfig convertMap(ValueMap map) { + com.electronwill.nightconfig.core.Config result = createUncommentedConfig(); + + for (Map.Entry entry : map.entrySet()) { + List key = new ArrayList<>(); + key.add(entry.getKey()); + result.add(key, convertAny(entry.getValue())); + } + + return result; + } + + private static Object convertAny(Object value) { + if (value instanceof ValueMap) { + return convertMap((ValueMap) value); + } else if (value instanceof ValueList) { + return convertList((ValueList) value); + } else if (value instanceof ConfigSerializableObject) { + return convertAny(((ConfigSerializableObject) value).getRepresentation()); + } else { + return value; + } + } + + private static com.electronwill.nightconfig.core.Config write(Config config, com.electronwill.nightconfig.core.Config uncommentedConfig, Iterable nodes) { + for (ValueTreeNode node : nodes) { + ValueKey key = SerializerUtils.getSerializedKey(config, node); + + if (node instanceof TrackedValue) { + TrackedValue value = (TrackedValue) node; + + uncommentedConfig.add(toNightConfigSerializable(key), convertAny(value.getRealValue())); + } else { + write(config, uncommentedConfig, ((ValueTreeNode.Section) node)); + } + } + + return uncommentedConfig; + } + + private static com.electronwill.nightconfig.core.Config createUncommentedConfig() { + return InMemoryFormat.defaultInstance().createConfig(LinkedHashMap::new); + } + + private static List toNightConfigSerializable(ValueKey key) { + List listKey = new ArrayList<>(); + key.forEach(listKey::add); + return listKey; + } +} diff --git a/config-common/src/main/java/band/kessoku/lib/config/api/serializers/night/TomlSerializer.java b/config-common/src/main/java/band/kessoku/lib/config/api/serializers/night/TomlSerializer.java new file mode 100644 index 00000000..e6df27d8 --- /dev/null +++ b/config-common/src/main/java/band/kessoku/lib/config/api/serializers/night/TomlSerializer.java @@ -0,0 +1,157 @@ +package band.kessoku.lib.config.api.serializers.night; + +import com.electronwill.nightconfig.core.CommentedConfig; +import com.electronwill.nightconfig.core.InMemoryCommentedFormat; +import com.electronwill.nightconfig.core.UnmodifiableCommentedConfig; +import com.electronwill.nightconfig.core.io.ConfigParser; +import com.electronwill.nightconfig.core.io.ConfigWriter; +import com.electronwill.nightconfig.toml.TomlParser; +import com.electronwill.nightconfig.toml.TomlWriter; +import org.quiltmc.config.api.Config; +import org.quiltmc.config.api.Constraint; +import org.quiltmc.config.api.MarshallingUtils; +import org.quiltmc.config.api.Serializer; +import org.quiltmc.config.api.annotations.Comment; +import org.quiltmc.config.api.values.ConfigSerializableObject; +import org.quiltmc.config.api.values.TrackedValue; +import org.quiltmc.config.api.values.ValueKey; +import org.quiltmc.config.api.values.ValueList; +import org.quiltmc.config.api.values.ValueMap; +import org.quiltmc.config.api.values.ValueTreeNode; +import org.quiltmc.config.impl.util.SerializerUtils; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * A default serializer that writes in the TOML format. + * + * @implNote When passing entries to {@link com.electronwill.nightconfig.core.Config#add(String, Object)}, the string key is automatically split at each dot ({@code .}). + * This completely breaks TOML serialization, since we allow dots in keys, using either {@link org.quiltmc.config.api.annotations.SerializedName} or {@link ValueMap}, whose keys are not validated for certain characters. + * To get around this, use {@link com.electronwill.nightconfig.core.Config#add(List, Object)} via passing your key into {@link #toNightConfigSerializable(ValueKey)}. + */ +public final class TomlSerializer implements Serializer { + public static final TomlSerializer INSTANCE = new TomlSerializer(); + private final ConfigParser parser = new TomlParser(); + private final ConfigWriter writer = new TomlWriter(); + + private TomlSerializer() { + + } + + @Override + public String getFileExtension() { + return "toml"; + } + + @Override + public void serialize(Config config, OutputStream to) { + this.writer.write(write(config, createCommentedConfig(), config.nodes()), to); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public void deserialize(Config config, InputStream from) { + CommentedConfig read = this.parser.parse(from); + + for (TrackedValue trackedValue : config.values()) { + List keyOptions = SerializerUtils.getPossibleKeys(config, trackedValue); + + for (ValueKey key : keyOptions) { + String stringKey = key.toString(); + + if (read.contains(stringKey)) { + ((TrackedValue) trackedValue).setValue(MarshallingUtils.coerce(read.get(stringKey), trackedValue.getDefaultValue(), (CommentedConfig c, MarshallingUtils.MapEntryConsumer entryConsumer) -> + c.entrySet().forEach(e -> entryConsumer.put(e.getKey(), e.getValue()))), false); + } + } + } + } + + private static List convertList(List list) { + List result = new ArrayList<>(list.size()); + + for (Object value : list) { + result.add(convertAny(value)); + } + + return result; + } + + private static UnmodifiableCommentedConfig convertMap(ValueMap map) { + CommentedConfig result = createCommentedConfig(); + + for (Map.Entry entry : map.entrySet()) { + List key = new ArrayList<>(); + key.add(entry.getKey()); + result.add(key, convertAny(entry.getValue())); + } + + return result; + } + + private static Object convertAny(Object value) { + if (value instanceof ValueMap) { + return convertMap((ValueMap) value); + } else if (value instanceof ValueList) { + return convertList((ValueList) value); + } else if (value instanceof ConfigSerializableObject) { + return convertAny(((ConfigSerializableObject) value).getRepresentation()); + } else { + return value; + } + } + + private static CommentedConfig write(Config config, CommentedConfig commentedConfig, Iterable nodes) { + for (ValueTreeNode node : nodes) { + List comments = new ArrayList<>(); + + if (node.hasMetadata(Comment.TYPE)) { + for (String string : node.metadata(Comment.TYPE)) { + comments.add(string); + } + } + + ValueKey key = SerializerUtils.getSerializedKey(config, node); + + if (node instanceof TrackedValue) { + TrackedValue value = (TrackedValue) node; + Object defaultValue = value.getDefaultValue(); + + SerializerUtils.createEnumOptionsComment(defaultValue).ifPresent(comments::add); + + for (Constraint constraint : value.constraints()) { + comments.add(constraint.getRepresentation()); + } + + Optional defaultValueComment = SerializerUtils.getDefaultValueString(defaultValue); + defaultValueComment.ifPresent(s -> comments.add("default: " + s)); + + commentedConfig.add(toNightConfigSerializable(key), convertAny(value.getRealValue())); + } else { + write(config, commentedConfig, ((ValueTreeNode.Section) node)); + } + + if (!comments.isEmpty()) { + commentedConfig.setComment(toNightConfigSerializable(key), " " + String.join("\n ", comments)); + } + } + + return commentedConfig; + } + + private static CommentedConfig createCommentedConfig() { + return InMemoryCommentedFormat.defaultInstance().createConfig(LinkedHashMap::new); + } + + private static List toNightConfigSerializable(ValueKey key) { + List listKey = new ArrayList<>(); + key.forEach(listKey::add); + return listKey; + } +} diff --git a/config-common/src/main/java/band/kessoku/lib/config/api/serializers/night/YamlSerializer.java b/config-common/src/main/java/band/kessoku/lib/config/api/serializers/night/YamlSerializer.java new file mode 100644 index 00000000..d2a41915 --- /dev/null +++ b/config-common/src/main/java/band/kessoku/lib/config/api/serializers/night/YamlSerializer.java @@ -0,0 +1,122 @@ +package band.kessoku.lib.config.api.serializers.night; + +import com.electronwill.nightconfig.core.*; +import com.electronwill.nightconfig.core.io.ConfigParser; +import com.electronwill.nightconfig.core.io.ConfigWriter; +import com.electronwill.nightconfig.yaml.YamlParser; +import com.electronwill.nightconfig.yaml.YamlWriter; +import org.quiltmc.config.api.Config; +import org.quiltmc.config.api.MarshallingUtils; +import org.quiltmc.config.api.Serializer; +import org.quiltmc.config.api.values.*; +import org.quiltmc.config.impl.util.SerializerUtils; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.*; + +/** + * A default serializer that writes in the YAML format. + * + * @implNote When passing entries to {@link com.electronwill.nightconfig.core.Config#add(String, Object)}, the string key is automatically split at each dot ({@code .}). + * This completely breaks TOML serialization, since we allow dots in keys, using either {@link org.quiltmc.config.api.annotations.SerializedName} or {@link ValueMap}, whose keys are not validated for certain characters. + * To get around this, use {@link com.electronwill.nightconfig.core.Config#add(List, Object)} via passing your key into {@link #toNightConfigSerializable(ValueKey)}. + */ +public final class YamlSerializer implements Serializer { + public static final YamlSerializer INSTANCE = new YamlSerializer(); + private final ConfigParser parser = new YamlParser(); + private final ConfigWriter writer = new YamlWriter(); + + private YamlSerializer() { + + } + + @Override + public String getFileExtension() { + return "toml"; + } + + @Override + public void serialize(Config config, OutputStream to) { + this.writer.write(write(config, createUncommentedConfig(), config.nodes()), to); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public void deserialize(Config config, InputStream from) { + com.electronwill.nightconfig.core.Config read = this.parser.parse(from); + + for (TrackedValue trackedValue : config.values()) { + List keyOptions = SerializerUtils.getPossibleKeys(config, trackedValue); + + for (ValueKey key : keyOptions) { + String stringKey = key.toString(); + + if (read.contains(stringKey)) { + ((TrackedValue) trackedValue).setValue(MarshallingUtils.coerce(read.get(stringKey), trackedValue.getDefaultValue(), (com.electronwill.nightconfig.core.Config c, MarshallingUtils.MapEntryConsumer entryConsumer) -> + c.entrySet().forEach(e -> entryConsumer.put(e.getKey(), e.getValue()))), false); + } + } + } + } + + private static List convertList(List list) { + List result = new ArrayList<>(list.size()); + + for (Object value : list) { + result.add(convertAny(value)); + } + + return result; + } + + private static UnmodifiableConfig convertMap(ValueMap map) { + com.electronwill.nightconfig.core.Config result = createUncommentedConfig(); + + for (Map.Entry entry : map.entrySet()) { + List key = new ArrayList<>(); + key.add(entry.getKey()); + result.add(key, convertAny(entry.getValue())); + } + + return result; + } + + private static Object convertAny(Object value) { + if (value instanceof ValueMap) { + return convertMap((ValueMap) value); + } else if (value instanceof ValueList) { + return convertList((ValueList) value); + } else if (value instanceof ConfigSerializableObject) { + return convertAny(((ConfigSerializableObject) value).getRepresentation()); + } else { + return value; + } + } + + private static com.electronwill.nightconfig.core.Config write(Config config, com.electronwill.nightconfig.core.Config uncommentedConfig, Iterable nodes) { + for (ValueTreeNode node : nodes) { + ValueKey key = SerializerUtils.getSerializedKey(config, node); + + if (node instanceof TrackedValue) { + TrackedValue value = (TrackedValue) node; + + uncommentedConfig.add(toNightConfigSerializable(key), convertAny(value.getRealValue())); + } else { + write(config, uncommentedConfig, ((ValueTreeNode.Section) node)); + } + } + + return uncommentedConfig; + } + + private static com.electronwill.nightconfig.core.Config createUncommentedConfig() { + return InMemoryFormat.defaultInstance().createConfig(LinkedHashMap::new); + } + + private static List toNightConfigSerializable(ValueKey key) { + List listKey = new ArrayList<>(); + key.forEach(listKey::add); + return listKey; + } +} diff --git a/config-common/src/main/java/band/kessoku/lib/config/api/serializers/quilt/Json5Serializer.java b/config-common/src/main/java/band/kessoku/lib/config/api/serializers/quilt/Json5Serializer.java new file mode 100644 index 00000000..4f09d111 --- /dev/null +++ b/config-common/src/main/java/band/kessoku/lib/config/api/serializers/quilt/Json5Serializer.java @@ -0,0 +1,229 @@ +package band.kessoku.lib.config.api.serializers.quilt; + +import org.quiltmc.config.api.Config; +import org.quiltmc.config.api.Constraint; +import org.quiltmc.config.api.MarshallingUtils; +import org.quiltmc.config.api.Serializer; +import org.quiltmc.config.api.annotations.Comment; +import org.quiltmc.config.api.exceptions.ConfigParseException; +import org.quiltmc.config.api.values.ConfigSerializableObject; +import org.quiltmc.config.api.values.TrackedValue; +import org.quiltmc.config.api.values.ValueKey; +import org.quiltmc.config.api.values.ValueList; +import org.quiltmc.config.api.values.ValueMap; +import org.quiltmc.config.api.values.ValueTreeNode; +import org.quiltmc.config.impl.tree.TrackedValueImpl; +import org.quiltmc.config.impl.util.SerializerUtils; +import org.quiltmc.parsers.json.JsonReader; +import org.quiltmc.parsers.json.JsonToken; +import org.quiltmc.parsers.json.JsonWriter; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * A default serializer that writes in the JSON5 format. + */ +public final class Json5Serializer implements Serializer { + public static final Json5Serializer INSTANCE = new Json5Serializer(); + + private Json5Serializer() { + + } + + @Override + public String getFileExtension() { + return "json5"; + } + + private void serialize(JsonWriter writer, Object value) throws IOException { + if (value instanceof Integer) { + writer.value((Integer) value); + } else if (value instanceof Long) { + writer.value((Long) value); + } else if (value instanceof Float) { + writer.value((Float) value); + } else if (value instanceof Double) { + writer.value((Double) value); + } else if (value instanceof Boolean) { + writer.value((Boolean) value); + } else if (value instanceof String) { + writer.value((String) value); + } else if (value instanceof ValueList) { + writer.beginArray(); + + for (Object v : (ValueList) value) { + serialize(writer, v); + } + + writer.endArray(); + } else if (value instanceof ValueMap) { + writer.beginObject(); + + for (Map.Entry entry : (ValueMap) value) { + writer.name(entry.getKey()); + serialize(writer, entry.getValue()); + } + + writer.endObject(); + } else if (value instanceof ConfigSerializableObject) { + serialize(writer, ((ConfigSerializableObject) value).getRepresentation()); + } else if (value == null) { + writer.nullValue(); + } else if (value.getClass().isEnum()) { + writer.value(((Enum) value).name()); + } else { + throw new ConfigParseException(); + } + } + + private void serialize(JsonWriter writer, ValueTreeNode node) throws IOException { + for (String comment : node.metadata(Comment.TYPE)) { + writer.comment(comment); + } + + if (node instanceof ValueTreeNode.Section) { + writer.name(node.key().getLastComponent()); + writer.beginObject(); + + for (ValueTreeNode child : ((ValueTreeNode.Section) node)) { + serialize(writer, child); + } + + writer.endObject(); + } else { + TrackedValue trackedValue = ((TrackedValue) node); + Object defaultValue = trackedValue.getDefaultValue(); + + Optional enumOptionsComment = SerializerUtils.createEnumOptionsComment(defaultValue); + if (enumOptionsComment.isPresent()) { + writer.comment(enumOptionsComment.get()); + } + + for (Constraint constraint : trackedValue.constraints()) { + writer.comment(constraint.getRepresentation()); + } + + Optional defaultComment = SerializerUtils.getDefaultValueString(defaultValue); + if (defaultComment.isPresent()) { + writer.comment("default: " + defaultComment.get()); + } + + String name = SerializerUtils.getSerializedName(trackedValue); + writer.name(name); + + serialize(writer, trackedValue.getRealValue()); + } + } + + @Override + public void serialize(Config config, OutputStream to) throws IOException { + JsonWriter writer = JsonWriter.json5(new OutputStreamWriter(to)); + + for (String comment : config.metadata(Comment.TYPE)) { + writer.comment(comment); + } + + writer.beginObject(); + + for (ValueTreeNode node : config.nodes()) { + this.serialize(writer, node); + } + + writer.endObject(); + writer.close(); + } + + @Override + @SuppressWarnings({"unchecked", "rawtypes"}) + public void deserialize(Config config, InputStream from) { + try { + JsonReader reader = JsonReader.json5(new InputStreamReader(from)); + + Map values = parseObject(reader); + + for (TrackedValue value : config.values()) { + Map m = values; + List keyOptions = SerializerUtils.getPossibleKeys(config, value); + + for (ValueKey key : keyOptions) { + for (int i = 0; i < key.length(); i++) { + String name = key.getKeyComponent(i); + if (m.containsKey(name) && i != key.length() - 1) { + m = (Map) m.get(name); + } else if (m.containsKey(name)) { + ((TrackedValueImpl) value).setValue(MarshallingUtils.coerce(m.get(name), value.getDefaultValue(), (Map map, MarshallingUtils.MapEntryConsumer entryConsumer) -> + map.forEach(entryConsumer::put)), false); + } + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static Map parseObject(JsonReader reader) throws IOException { + reader.beginObject(); + + Map object = new LinkedHashMap<>(); + + while (reader.hasNext() && reader.peek() == JsonToken.NAME) { + object.put(reader.nextName(), parseElement(reader)); + } + + reader.endObject(); + + return object; + } + + public static List parseArray(JsonReader reader) throws IOException { + reader.beginArray(); + + List array = new ArrayList<>(); + + while (reader.hasNext() && reader.peek() != JsonToken.END_ARRAY) { + array.add(parseElement(reader)); + } + + reader.endArray(); + + return array; + } + + private static Object parseElement(JsonReader reader) throws IOException { + switch (reader.peek()) { + case END_ARRAY: + throw new ConfigParseException("Unexpected end of array"); + case BEGIN_OBJECT: + return parseObject(reader); + case BEGIN_ARRAY: + return parseArray(reader); + case END_OBJECT: + throw new ConfigParseException("Unexpected end of object"); + case NAME: + throw new ConfigParseException("Unexpected name"); + case STRING: + return reader.nextString(); + case NUMBER: + return reader.nextNumber(); + case BOOLEAN: + return reader.nextBoolean(); + case NULL: + reader.nextNull(); + return null; + case END_DOCUMENT: + throw new ConfigParseException("Unexpected end of file"); + } + + throw new ConfigParseException("Encountered unknown JSON token"); + } +} diff --git a/config-common/src/main/java/band/kessoku/lib/config/impl/ModConfigHelperImpl.java b/config-common/src/main/java/band/kessoku/lib/config/impl/ModConfigHelperImpl.java new file mode 100644 index 00000000..6d2da8f0 --- /dev/null +++ b/config-common/src/main/java/band/kessoku/lib/config/impl/ModConfigHelperImpl.java @@ -0,0 +1,69 @@ +package band.kessoku.lib.config.impl; + +import band.kessoku.lib.base.ModUtils; +import band.kessoku.lib.config.api.serializers.night.HoconSerializer; +import band.kessoku.lib.config.api.serializers.night.JsonSerializer; +import band.kessoku.lib.config.api.serializers.night.YamlSerializer; +import band.kessoku.lib.config.api.serializers.quilt.Json5Serializer; +import band.kessoku.lib.config.api.serializers.night.TomlSerializer; +import band.kessoku.lib.platform.api.ModLoader; + +import org.quiltmc.config.api.Serializer; +import org.quiltmc.config.implementor_api.ConfigEnvironment; +import org.slf4j.Logger; + +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Map; + +public final class ModConfigHelperImpl { + private static ConfigEnvironment ENV; + private static final Path CONFIG_DIR = ModLoader.getInstance().getConfigFolder(); + private static final Logger LOGGER = ModUtils.getLogger(); + + public static void init() { + Map serializerMap = new LinkedHashMap<>(); + + serializerMap.put("json5", Json5Serializer.INSTANCE); + + serializerMap.put("toml", TomlSerializer.INSTANCE); + serializerMap.put("json", JsonSerializer.INSTANCE); + serializerMap.put("yaml", YamlSerializer.INSTANCE); + serializerMap.put("hocon", HoconSerializer.INSTANCE); + + String globalConfigExtension = System.getProperty("kessoku.config.globalConfigExtension"); + String defaultConfigExtension = System.getProperty("kessoku.config.defaultConfigExtension"); + + Serializer[] serializers = serializerMap.values().toArray(new Serializer[0]); + + if (globalConfigExtension != null && !serializerMap.containsKey(globalConfigExtension)) { + throw new RuntimeException("Cannot use file extension " + globalConfigExtension + " globally: no matching serializer found"); + } + + if (defaultConfigExtension != null && !serializerMap.containsKey(defaultConfigExtension)) { + throw new RuntimeException("Cannot use file extension " + defaultConfigExtension + " by default: no matching serializer found"); + } + + if (defaultConfigExtension == null) { + ENV = new ConfigEnvironment(CONFIG_DIR, globalConfigExtension, serializers[0]); + + for (int i = 1; i < serializers.length; ++i) { + ENV.registerSerializer(serializers[i]); + } + } else { + ENV = new ConfigEnvironment(CONFIG_DIR, globalConfigExtension, serializerMap.get(defaultConfigExtension)); + + for (Serializer serializer : serializers) { + ENV.registerSerializer(serializer); + } + } + } + + public static ConfigEnvironment getConfigEnvironment() { + return ENV; + } + + private ModConfigHelperImpl() { + + } +} diff --git a/config-common/src/main/resources/icon.png b/config-common/src/main/resources/icon.png new file mode 100644 index 00000000..226df8cf Binary files /dev/null and b/config-common/src/main/resources/icon.png differ diff --git a/config-fabric/build.gradle b/config-fabric/build.gradle new file mode 100644 index 00000000..adb2e8de --- /dev/null +++ b/config-fabric/build.gradle @@ -0,0 +1,12 @@ +apply from: rootProject.file("gradle/scripts/kessokulib-fabric.gradle") + +group = "band.kessoku.lib.config" +base.archivesName = rootProject.name + "-config" + +dependencies { + moduleImplementation(project(":base-common")) + moduleImplementation(project(":platform-common")) + + common(project(path: ':config-common', configuration: 'namedElements')) { transitive false } + shadowBundle project(path: ':config-common', configuration: 'transformProductionFabric') +} \ No newline at end of file diff --git a/config-fabric/src/main/java/band/kessoku/lib/config/KessokuConfigEntrypoint.java b/config-fabric/src/main/java/band/kessoku/lib/config/KessokuConfigEntrypoint.java new file mode 100644 index 00000000..d8bdddff --- /dev/null +++ b/config-fabric/src/main/java/band/kessoku/lib/config/KessokuConfigEntrypoint.java @@ -0,0 +1,11 @@ +package band.kessoku.lib.config; + +import band.kessoku.lib.config.impl.ModConfigHelperImpl; +import net.fabricmc.api.ModInitializer; + +public class KessokuConfigEntrypoint implements ModInitializer { + @Override + public void onInitialize() { + ModConfigHelperImpl.init(); + } +} diff --git a/config-fabric/src/main/resources/fabric.mod.json b/config-fabric/src/main/resources/fabric.mod.json new file mode 100644 index 00000000..ff234944 --- /dev/null +++ b/config-fabric/src/main/resources/fabric.mod.json @@ -0,0 +1,31 @@ +{ + "schemaVersion": 1, + "id": "kessoku_config", + "version": "${version}", + "name": "Kessoku Config API", + "description": "A simple config api. Based quilt-config.", + "authors": [ + "Kessoku Tea Time" + ], + "contact": { + "homepage": "https://modrinth.com/mod/kessoku-lib", + "sources": "https://github.com/KessokuTeaTime/KessokuLib", + "issues": "https://github.com/KessokuTeaTime/KessokuLib/issues" + }, + "license": "LGPL-3.0-only", + "icon": "icon.png", + "environment": "*", + "depends": { + "fabricloader": ">=0.16.0", + "minecraft": "1.21", + "java": ">=21", + "fabric-api": "*" + }, + "custom": { + "modmenu": { + "badges": [ + "library" + ] + } + } +} \ No newline at end of file diff --git a/config-neo/build.gradle b/config-neo/build.gradle new file mode 100644 index 00000000..3a2af6af --- /dev/null +++ b/config-neo/build.gradle @@ -0,0 +1,12 @@ +apply from: rootProject.file("gradle/scripts/kessokulib-neo.gradle") + +group = "band.kessoku.lib.config" +base.archivesName = rootProject.name + "-config" + +dependencies { + moduleImplementation(project(":base-common")) + moduleImplementation(project(":platform-common")) + + common(project(path: ':config-common', configuration: 'namedElements')) { transitive false } + shadowBundle project(path: ':config-common', configuration: 'transformProductionNeoForge') +} \ No newline at end of file diff --git a/config-neo/gradle.properties b/config-neo/gradle.properties new file mode 100644 index 00000000..2914393d --- /dev/null +++ b/config-neo/gradle.properties @@ -0,0 +1 @@ +loom.platform=neoforge \ No newline at end of file diff --git a/config-neo/src/main/java/band/kessoku/lib/config/KessokuConfigEntrypoint.java b/config-neo/src/main/java/band/kessoku/lib/config/KessokuConfigEntrypoint.java new file mode 100644 index 00000000..e68e7367 --- /dev/null +++ b/config-neo/src/main/java/band/kessoku/lib/config/KessokuConfigEntrypoint.java @@ -0,0 +1,11 @@ +package band.kessoku.lib.config; + +import band.kessoku.lib.config.impl.ModConfigHelperImpl; +import net.neoforged.fml.common.Mod; + +@Mod(KessokuConfig.MOD_ID) +public class KessokuConfigEntrypoint { + public KessokuConfigEntrypoint() { + ModConfigHelperImpl.init(); + } +} diff --git a/config-neo/src/main/resources/META-INF/neoforge.mods.toml b/config-neo/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 00000000..1d988e2a --- /dev/null +++ b/config-neo/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,29 @@ +modLoader = "javafml" +loaderVersion = "[4,)" +license = "LGPL-3.0-only" +issueTrackerURL = "https://github.com/KessokuTeaTime/KessokuLib/issues" + +[[mods]] +modId = "kessoku_config" +version = "${version}" +displayName = "Kessoku Config API" +description = ''' +A simple config api. Based quilt-config. +''' +logoFile = "icon.png" +authors = "Kessoku Tea Time" +displayURL = "https://modrinth.com/mod/kessoku-lib" + +[[dependencies.kessoku_config]] +modId = "neoforge" +type = "required" +versionRange = "[21.0,)" +ordering = "NONE" +side = "BOTH" + +[[dependencies.kessoku_config]] +modId = "minecraft" +type = "required" +versionRange = "[1.21,)" +ordering = "NONE" +side = "BOTH" \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0be10e08..008d802e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,11 @@ architectury = "3.4-SNAPSHOT" shadow = "8.1.1" spotless = "6.25.0" +# Config libraries +night-config = "3.8.1" +quilt-config = "1.3.1" +quilt-json-parsers = "0.2.0" + [libraries] minecraft = { group = "com.mojang", name = "minecraft", version.ref = "minecraft" } yarn = { group = "net.fabricmc", name = "yarn", version.ref = "yarn" } @@ -29,8 +34,22 @@ fabric-loader = { group = "net.fabricmc", name = "fabric-loader", version.ref = # Mods fabric-api = { group = "net.fabricmc.fabric-api", name = "fabric-api", version.ref = "fabric-api" } +# Config libraries +night-config-core = { group = "com.electronwill.night-config", name = "core", version.ref = "night-config" } +night-config-toml = { group = "com.electronwill.night-config", name = "toml", version.ref = "night-config" } +night-config-json = { group = "com.electronwill.night-config", name = "json", version.ref = "night-config" } +night-config-yaml = { group = "com.electronwill.night-config", name = "yaml", version.ref = "night-config" } +night-config-hocon = { group = "com.electronwill.night-config", name = "hocon", version.ref = "night-config" } + +quilt-config = { group = "org.quiltmc", name = "quilt-config", version.ref = "quilt-config" } +quilt-json-parsers = { group = "org.quiltmc.parsers", name = "json", version.ref = "quilt-json-parsers" } + [plugins] loom = { id = "dev.architectury.loom", version.ref = "loom" } architectury = { id = "architectury-plugin", version.ref = "architectury" } shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" } -spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } \ No newline at end of file +spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } + +[bundles] +night-config = ["night-config-core", "night-config-toml", "night-config-json", "night-config-yaml", "night-config-hocon"] +quilt-config = ["quilt-config", "quilt-json-parsers"] \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 8a0f20a0..36f7ece0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,4 +17,5 @@ include("registry-common", "registry-fabric", "registry-neo") // Registry include("lifecycle-events-common", "lifecycle-events-fabric", "lifecycle-events-neo") // Lifecycle Events include("command-common", "command-fabric", "command-neo") // Command API include("keybind-common", "keybind-fabric", "keybind-neo") // Keybind API +include("config-common", "config-fabric", "config-neo") // Config API