[config] Expose tooltips/titles as comments in json

This commit is contained in:
Johannes Frohnmeyer 2022-04-03 18:17:14 +02:00
parent 5dc4198077
commit 4a65a61b7e
Signed by: Johannes
GPG Key ID: E76429612C2929F4
10 changed files with 96 additions and 44 deletions

View File

@ -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

View File

@ -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.

View File

@ -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 {
}

View File

@ -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;
}
}

View File

@ -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 {
}

View File

@ -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<EntryInfo> getEntries();
Map<String, Runnable> getPresets();
List<String> getReferencedConfigs();
Map<String, ConfigInstance> 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();
}

View File

@ -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<String, ConfigInstance> configs = new HashMap<>();
private final Map<Path, ConfigInstance> configsByPath = new HashMap<>();

View File

@ -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<String> referencedConfigs;
public final List<EntryInfo> entries = new ArrayList<>();
public final Map<String, Runnable> 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<String, ConfigInstance> 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<String, ConfigInstance> 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<String, ConfigInstance> 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

View File

@ -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);
}

View File

@ -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<String, String> TRANSLATION_SUPPLIER = s -> null;
@Override
public void onPreLaunch() {
for (EntrypointContainer<JfConfig> 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) {