diff --git a/docs/libjf-base.md b/docs/libjf-base.md index d451d3c..a2d3603 100644 --- a/docs/libjf-base.md +++ b/docs/libjf-base.md @@ -3,7 +3,7 @@ libjf-base is a dependency of all other modules and provides common functionalit It has no dependencies. It includes: -- a Gson strategy to ignore fields annotated with GsonHidden +- a Gson strategy to ignore fields annotated with GsonHidden, ClientOnly and ServerOnly - LazySupplier, a supplier that caches the result of another supplier to which it delegates - ThrowingRunnable and ThrowingSupplier, counterparts of their default lambdas which allow exceptions - a "flags" system to allow dynamically enabling LibJF features through system properties and fabric.mod.json diff --git a/docs/libjf-config-v0.md b/docs/libjf-config-v0.md index 4e05d43..9fceca4 100644 --- a/docs/libjf-config-v0.md +++ b/docs/libjf-config-v0.md @@ -24,7 +24,7 @@ public class TestConfig implements JfConfig { } ``` You MUST annotate any field configurable through the UI as @Entry and the class MUST extend JfConfig. -You MAY annotate fields as @GsonHidden to not serialize them (-> [libjf-base](libjf-base.md)). +You MAY annotate fields as @GsonHidden, @ClientOnly or @ServerOnly to hide them from the file as well them (-> [libjf-base](libjf-base.md)). Numeric values MAY have a min and max value specified in their @Entry. To register a config, add a `libjf:config` entrypoint pointing to its class to your fabric.mod.json. diff --git a/libjf-base/src/main/java/io/gitlab/jfronny/libjf/gson/ClientOnly.java b/libjf-base/src/main/java/io/gitlab/jfronny/libjf/gson/ClientOnly.java new file mode 100644 index 0000000..fbe829d --- /dev/null +++ b/libjf-base/src/main/java/io/gitlab/jfronny/libjf/gson/ClientOnly.java @@ -0,0 +1,11 @@ +package io.gitlab.jfronny.libjf.gson; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface ClientOnly { +} diff --git a/libjf-base/src/main/java/io/gitlab/jfronny/libjf/gson/HiddenAnnotationExclusionStrategy.java b/libjf-base/src/main/java/io/gitlab/jfronny/libjf/gson/HiddenAnnotationExclusionStrategy.java index b69e6fa..908f956 100644 --- a/libjf-base/src/main/java/io/gitlab/jfronny/libjf/gson/HiddenAnnotationExclusionStrategy.java +++ b/libjf-base/src/main/java/io/gitlab/jfronny/libjf/gson/HiddenAnnotationExclusionStrategy.java @@ -2,12 +2,17 @@ package io.gitlab.jfronny.libjf.gson; import io.gitlab.jfronny.gson.ExclusionStrategy; import io.gitlab.jfronny.gson.FieldAttributes; +import net.fabricmc.api.EnvType; +import net.fabricmc.loader.api.FabricLoader; public class HiddenAnnotationExclusionStrategy implements ExclusionStrategy { public boolean shouldSkipClass(Class clazz) { return false; } public boolean shouldSkipField(FieldAttributes fieldAttributes) { - return fieldAttributes.getAnnotation(GsonHidden.class) != null; + if (fieldAttributes.getAnnotation(GsonHidden.class) != null) return true; + return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT + ? fieldAttributes.getAnnotation(ServerOnly.class) != null + : fieldAttributes.getAnnotation(ClientOnly.class) != null; } } diff --git a/libjf-base/src/main/java/io/gitlab/jfronny/libjf/gson/ServerOnly.java b/libjf-base/src/main/java/io/gitlab/jfronny/libjf/gson/ServerOnly.java new file mode 100644 index 0000000..3081902 --- /dev/null +++ b/libjf-base/src/main/java/io/gitlab/jfronny/libjf/gson/ServerOnly.java @@ -0,0 +1,11 @@ +package io.gitlab.jfronny.libjf.gson; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface ServerOnly { +} diff --git a/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/api/ConfigInstance.java b/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/api/ConfigInstance.java index fc554ea..7eedc8a 100644 --- a/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/api/ConfigInstance.java +++ b/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/api/ConfigInstance.java @@ -1,8 +1,11 @@ package io.gitlab.jfronny.libjf.config.api; import io.gitlab.jfronny.gson.JsonObject; +import io.gitlab.jfronny.gson.stream.JsonWriter; import io.gitlab.jfronny.libjf.config.impl.EntryInfo; +import org.jetbrains.annotations.ApiStatus; +import java.io.IOException; import java.util.List; import java.util.Map; @@ -15,15 +18,16 @@ public interface ConfigInstance { } void load(); void write(); - void loadObject(JsonObject source); - JsonObject writeObject(); - void syncToClass(); - void syncFromClass(); String getModId(); - boolean matchesConfigClass(Class candidate); List getEntries(); Map getPresets(); List getReferencedConfigs(); Map getCategories(); - String getCategoryPath(); + + @ApiStatus.Internal void syncToClass(); + @ApiStatus.Internal void syncFromClass(); + @ApiStatus.Internal boolean matchesConfigClass(Class candidate); + @ApiStatus.Internal void loadFrom(JsonObject source); + @ApiStatus.Internal void writeTo(JsonWriter writer) throws IOException; + @ApiStatus.Internal String getCategoryPath(); } diff --git a/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/ConfigHolderImpl.java b/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/ConfigHolderImpl.java index 643fb64..f95cd4d 100644 --- a/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/ConfigHolderImpl.java +++ b/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/ConfigHolderImpl.java @@ -2,9 +2,11 @@ package io.gitlab.jfronny.libjf.config.impl; import com.google.common.collect.ImmutableMap; import io.gitlab.jfronny.gson.Gson; +import io.gitlab.jfronny.gson.GsonBuilder; import io.gitlab.jfronny.libjf.LibJf; import io.gitlab.jfronny.libjf.config.api.ConfigHolder; import io.gitlab.jfronny.libjf.config.api.ConfigInstance; +import io.gitlab.jfronny.libjf.gson.HiddenAnnotationExclusionStrategy; import io.gitlab.jfronny.libjf.unsafe.JfLanguageAdapter; import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.ModContainer; @@ -12,6 +14,7 @@ import net.fabricmc.loader.api.metadata.CustomValue; import net.fabricmc.loader.impl.util.log.Log; import org.jetbrains.annotations.ApiStatus; +import java.lang.reflect.Modifier; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; @@ -23,7 +26,12 @@ public class ConfigHolderImpl implements ConfigHolder { private ConfigHolderImpl() { } public static final String MODULE_ID = LibJf.MOD_ID + ":config"; - public static final Gson GSON = new Gson(); + public static Gson GSON = new GsonBuilder() + .excludeFieldsWithModifiers(Modifier.TRANSIENT) + .excludeFieldsWithModifiers(Modifier.PRIVATE) + .addSerializationExclusionStrategy(new HiddenAnnotationExclusionStrategy()) + .setPrettyPrinting() + .create(); private final Map configs = new HashMap<>(); private final Map configsByPath = new HashMap<>(); diff --git a/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/ConfigInstanceAbstract.java b/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/ConfigInstanceAbstract.java index 195e355..24dbf2d 100644 --- a/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/ConfigInstanceAbstract.java +++ b/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/ConfigInstanceAbstract.java @@ -2,23 +2,24 @@ package io.gitlab.jfronny.libjf.config.impl; import io.gitlab.jfronny.gson.JsonElement; import io.gitlab.jfronny.gson.JsonObject; +import io.gitlab.jfronny.gson.stream.JsonWriter; import io.gitlab.jfronny.libjf.config.api.*; -import io.gitlab.jfronny.libjf.generic.Try; +import io.gitlab.jfronny.libjf.config.impl.entrypoint.JfConfigSafe; import io.gitlab.jfronny.libjf.unsafe.JfLanguageAdapter; import net.fabricmc.loader.impl.util.log.Log; -import java.lang.reflect.Constructor; +import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.*; +import java.util.stream.Collectors; public abstract class ConfigInstanceAbstract implements ConfigInstance { public static final String CONFIG_PRESET_DEFAULT = "libjf-config-v0.default"; public final String modId; private final String categoryPath; public final Class configClass; - private final Constructor configConstructor; public final List referencedConfigs; public final List entries = new ArrayList<>(); public final Map presets = new LinkedHashMap<>(); @@ -28,10 +29,6 @@ public abstract class ConfigInstanceAbstract implements ConfigInstance { this.modId = modId; this.categoryPath = categoryPath; this.configClass = configClass; - this.configConstructor = Try.orElse(configClass::getDeclaredConstructor, e -> { - Log.error(JfLanguageAdapter.LOG_CATEGORY, "Could not get constructor for config class of " + modId + ", saving will be unavailable", e); - return null; - }); this.referencedConfigs = List.copyOf(meta.referencedConfigs); for (Field field : configClass.getFields()) { EntryInfo info = new EntryInfo(); @@ -120,32 +117,43 @@ public abstract class ConfigInstanceAbstract implements ConfigInstance { } @Override - public void loadObject(JsonObject source) { - ConfigHolderImpl.GSON.fromJson(source, configClass); + public void loadFrom(JsonObject source) { + for (EntryInfo entry : entries) { + if (source.has(entry.field.getName())) { + JsonElement el = source.get(entry.field.getName()); + entry.value = ConfigHolderImpl.GSON.fromJson(el, entry.field.getGenericType()); + } else Log.error(JfLanguageAdapter.LOG_CATEGORY, "Config does not contain entry for " + entry.field.getName()); + } for (Map.Entry entry : subcategories.entrySet()) { if (source.has(entry.getKey())) { JsonElement el = source.get(entry.getKey()); - if (el.isJsonObject()) entry.getValue().loadObject(el.getAsJsonObject()); + if (el.isJsonObject()) entry.getValue().loadFrom(el.getAsJsonObject()); else Log.error(JfLanguageAdapter.LOG_CATEGORY, "Config category is not a JSON object, skipping"); - } + } else Log.error(JfLanguageAdapter.LOG_CATEGORY, "Config does not contain entry for subcategory " + entry.getKey()); } + syncToClass(); } @Override - public JsonObject writeObject() { - try { - if (configConstructor == null) { - Log.error(JfLanguageAdapter.LOG_CATEGORY, "Could not save config of " + modId + " due to a missing constructor"); - return new JsonObject(); - } - JsonObject jo = ConfigHolderImpl.GSON.toJsonTree(configConstructor.newInstance()).getAsJsonObject(); - for (Map.Entry entry : subcategories.entrySet()) { - jo.add(entry.getKey(), entry.getValue().writeObject()); - } - return jo; - } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException("Could not access constructor needed to generate config JSON", e); + public void writeTo(JsonWriter writer) throws IOException { + syncFromClass(); + writer.beginObject(); + String val; + for (EntryInfo entry : entries) { + if ((val = JfConfigSafe.TRANSLATION_SUPPLIER.apply(modId + ".jfconfig." + categoryPath + entry.field.getName() + ".tooltip")) != null) + writer.comment(val); + if (entry.field.getType().isEnum()) + writer.comment("Valid: [" + Arrays.stream(entry.field.getType().getEnumConstants()).map(Objects::toString).collect(Collectors.joining(", ")) + "]"); + writer.name(entry.field.getName()); + ConfigHolderImpl.GSON.toJson(entry.value, entry.field.getGenericType(), writer); } + for (Map.Entry entry : subcategories.entrySet()) { + if ((val = JfConfigSafe.TRANSLATION_SUPPLIER.apply(modId + ".jfconfig." + categoryPath + entry.getKey() + ".title")) != null) + writer.comment(val); + writer.name(entry.getKey()); + entry.getValue().writeTo(writer); + } + writer.endObject(); } @Override @@ -158,9 +166,6 @@ public abstract class ConfigInstanceAbstract implements ConfigInstance { } } syncFromClass(); - for (ConfigInstance instance : subcategories.values()) { - instance.syncToClass(); - } } @Override @@ -177,9 +182,6 @@ public abstract class ConfigInstanceAbstract implements ConfigInstance { Log.error(JfLanguageAdapter.LOG_CATEGORY, "Could not read value", e); } } - for (ConfigInstance instance : subcategories.values()) { - instance.syncFromClass(); - } } @Override diff --git a/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/ConfigInstanceRoot.java b/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/ConfigInstanceRoot.java index c6c847f..676f8a5 100644 --- a/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/ConfigInstanceRoot.java +++ b/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/ConfigInstanceRoot.java @@ -2,6 +2,7 @@ package io.gitlab.jfronny.libjf.config.impl; import io.gitlab.jfronny.gson.JsonElement; import io.gitlab.jfronny.gson.JsonParser; +import io.gitlab.jfronny.gson.stream.JsonWriter; import io.gitlab.jfronny.libjf.unsafe.JfLanguageAdapter; import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.impl.util.log.Log; @@ -27,22 +28,22 @@ public class ConfigInstanceRoot extends ConfigInstanceAbstract { if (Files.exists(path)) { try (BufferedReader br = Files.newBufferedReader(path)) { JsonElement element = JsonParser.parseReader(br); - if (element.isJsonObject()) loadObject(element.getAsJsonObject()); + if (element.isJsonObject()) loadFrom(element.getAsJsonObject()); else Log.error(JfLanguageAdapter.LOG_CATEGORY, "Invalid config: Not a JSON object for " + modId); } catch (Exception e) { Log.error(JfLanguageAdapter.LOG_CATEGORY, "Could not read config for " + modId, e); } } - syncFromClass(); write(); } @Override public void write() { JfConfigWatchService.lock(path, () -> { - try (BufferedWriter bw = Files.newBufferedWriter(path)) { - ConfigHolderImpl.GSON.toJson(writeObject(), bw); + try (BufferedWriter bw = Files.newBufferedWriter(path); + JsonWriter jw = ConfigHolderImpl.GSON.newJsonWriter(bw)) { + writeTo(jw); } catch (Exception e) { Log.error(JfLanguageAdapter.LOG_CATEGORY, "Could not write config", e); } diff --git a/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/entrypoint/JfConfigSafe.java b/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/entrypoint/JfConfigSafe.java index 29829c3..8bab9fe 100644 --- a/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/entrypoint/JfConfigSafe.java +++ b/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/entrypoint/JfConfigSafe.java @@ -1,5 +1,6 @@ package io.gitlab.jfronny.libjf.config.impl.entrypoint; +import io.gitlab.jfronny.libjf.LibJf; import io.gitlab.jfronny.libjf.config.api.ConfigHolder; import io.gitlab.jfronny.libjf.config.api.JfConfig; import io.gitlab.jfronny.libjf.config.impl.ConfigHolderImpl; @@ -8,13 +9,22 @@ import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.entrypoint.EntrypointContainer; import net.fabricmc.loader.api.entrypoint.PreLaunchEntrypoint; import net.fabricmc.loader.impl.util.log.Log; +import net.minecraft.util.Language; + +import java.util.function.Function; public class JfConfigSafe implements PreLaunchEntrypoint { + public static Function TRANSLATION_SUPPLIER = s -> null; @Override public void onPreLaunch() { for (EntrypointContainer config : FabricLoader.getInstance().getEntrypointContainers(ConfigHolderImpl.MODULE_ID, JfConfig.class)) { registerIfMissing(config.getProvider().getMetadata().getId(), config.getEntrypoint().getClass()); } + TRANSLATION_SUPPLIER = s -> { + String translated = Language.getInstance().get(s); + return translated.equals(s) ? null : translated; + }; + ConfigHolderImpl.GSON = LibJf.GSON; } public static void registerIfMissing(String modId, Class klazz) {