[config] Expose tooltips/titles as comments in json
This commit is contained in:
parent
5dc4198077
commit
4a65a61b7e
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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<>();
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue