From 1964cccf9061c2e6bf0527b1de15a20f98f3cc2b Mon Sep 17 00:00:00 2001 From: JFronny Date: Thu, 31 Mar 2022 20:45:10 +0200 Subject: [PATCH] [config] Category support pt 2 --- .../gitlab/jfronny/libjf/interfaces/Try.java | 22 +++++ .../libjf/config/api/ConfigInstance.java | 5 +- .../config/impl/ConfigInstanceAbstract.java | 48 ++++++++++- .../libjf/config/impl/ConfigInstanceRoot.java | 34 ++++---- .../jfronny/libjf/config/impl/EntryInfo.java | 9 +- .../client/screen/EntryInfoWidgetBuilder.java | 84 ++++++++++++------- .../impl/client/screen/TinyConfigScreen.java | 33 ++------ 7 files changed, 163 insertions(+), 72 deletions(-) create mode 100644 libjf-base/src/main/java/io/gitlab/jfronny/libjf/interfaces/Try.java diff --git a/libjf-base/src/main/java/io/gitlab/jfronny/libjf/interfaces/Try.java b/libjf-base/src/main/java/io/gitlab/jfronny/libjf/interfaces/Try.java new file mode 100644 index 0000000..88fc876 --- /dev/null +++ b/libjf-base/src/main/java/io/gitlab/jfronny/libjf/interfaces/Try.java @@ -0,0 +1,22 @@ +package io.gitlab.jfronny.libjf.interfaces; + +import java.util.function.Consumer; +import java.util.function.Function; + +public class Try { + public static void orElse(ThrowingRunnable tr, Consumer alternative) { + try { + tr.run(); + } catch (Throwable e) { + alternative.accept(e); + } + } + + public static T orElse(ThrowingSupplier tr, Function alternative) { + try { + return tr.get(); + } catch (Throwable e) { + return alternative.apply(e); + } + } +} 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 d5233a1..634fd98 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,5 +1,6 @@ package io.gitlab.jfronny.libjf.config.api; +import com.google.gson.JsonObject; import io.gitlab.jfronny.libjf.config.impl.EntryInfo; import java.util.List; @@ -13,9 +14,11 @@ public interface ConfigInstance { return ConfigHolder.getInstance().get(modId); } void load(); + void write(); + void loadObject(JsonObject source); + JsonObject writeObject(); void syncToClass(); void syncFromClass(); - void write(); String getModId(); boolean matchesConfigClass(Class candidate); List getEntries(); 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 7040dc0..9fbaca6 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 @@ -1,8 +1,12 @@ package io.gitlab.jfronny.libjf.config.impl; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import io.gitlab.jfronny.libjf.LibJf; +import io.gitlab.jfronny.libjf.interfaces.Try; import io.gitlab.jfronny.libjf.config.api.*; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -12,6 +16,7 @@ public abstract class ConfigInstanceAbstract implements ConfigInstance { 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<>(); @@ -21,6 +26,10 @@ public abstract class ConfigInstanceAbstract implements ConfigInstance { this.modId = modId; this.categoryPath = categoryPath; this.configClass = configClass; + this.configConstructor = Try.orElse(configClass::getDeclaredConstructor, e -> { + LibJf.LOGGER.error("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(); @@ -95,13 +104,48 @@ public abstract class ConfigInstanceAbstract implements ConfigInstance { for (Class categoryClass : configClass.getClasses()) { if (categoryClass.isAnnotationPresent(Category.class)) { - String path = categoryPath + categoryClass.getSimpleName() + "."; + String name = camelCase(categoryClass.getSimpleName()); //TODO allow custom auxiliary metadata - subcategories.put(path, new ConfigInstanceCategory(this, modId, path, categoryClass, new AuxiliaryMetadata().sanitize())); + subcategories.put(name, new ConfigInstanceCategory(this, modId, categoryPath + name + ".", categoryClass, new AuxiliaryMetadata().sanitize())); } } } + private String camelCase(String source) { + if (source == null) return null; + if (source.length() == 0) return source; + return Character.toLowerCase(source.charAt(0)) + source.substring(1); + } + + @Override + public void loadObject(JsonObject source) { + LibJf.GSON.fromJson(source, configClass); + 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()); + else LibJf.LOGGER.error("Config category is not a JSON object, skipping"); + } + } + } + + @Override + public JsonObject writeObject() { + try { + if (configConstructor == null) { + LibJf.LOGGER.error("Could not save config of " + modId + " due to a missing constructor"); + return new JsonObject(); + } + JsonObject jo = LibJf.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); + } + } + @Override public void syncToClass() { for (EntryInfo info : entries) { 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 b150233..c0ac686 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 @@ -1,8 +1,12 @@ package io.gitlab.jfronny.libjf.config.impl; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; import io.gitlab.jfronny.libjf.LibJf; import net.fabricmc.loader.api.FabricLoader; +import java.io.BufferedReader; +import java.io.BufferedWriter; import java.nio.file.Files; import java.nio.file.Path; @@ -14,17 +18,20 @@ public class ConfigInstanceRoot extends ConfigInstanceAbstract { public ConfigInstanceRoot(String modId, Class config, AuxiliaryMetadata meta) { super(modId, "", config, meta); path = FabricLoader.getInstance().getConfigDir().resolve(modId + ".json"); - load(); } @Override public void load() { - try { - LibJf.GSON.fromJson(Files.newBufferedReader(path), configClass); - } - catch (Exception e) { - LibJf.LOGGER.error("Could not read config", e); + if (Files.exists(path)) { + try (BufferedReader br = Files.newBufferedReader(path)) { + JsonElement element = JsonParser.parseReader(br); + if (element.isJsonObject()) loadObject(element.getAsJsonObject()); + else LibJf.LOGGER.error("Invalid config: Not a JSON object for " + modId); + } + catch (Exception e) { + LibJf.LOGGER.error("Could not read config for " + modId, e); + } } syncFromClass(); write(); @@ -32,13 +39,12 @@ public class ConfigInstanceRoot extends ConfigInstanceAbstract { @Override public void write() { - try { - JfConfigWatchService.lock(path, () -> { - if (!Files.exists(path)) Files.createFile(path); - Files.write(path, LibJf.GSON.toJson(configClass.getDeclaredConstructor().newInstance()).getBytes()); - }); - } catch (Exception e) { - LibJf.LOGGER.error("Could not write config", e); - } + JfConfigWatchService.lock(path, () -> { + try (BufferedWriter bw = Files.newBufferedWriter(path)) { + LibJf.GSON.toJson(writeObject(), bw); + } catch (Exception e) { + LibJf.LOGGER.error("Could not write config", e); + } + }); } } diff --git a/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/EntryInfo.java b/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/EntryInfo.java index 38c5490..3d17da4 100644 --- a/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/EntryInfo.java +++ b/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/EntryInfo.java @@ -1,6 +1,9 @@ package io.gitlab.jfronny.libjf.config.impl; import io.gitlab.jfronny.libjf.config.api.Entry; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.ClickableWidget; import net.minecraft.client.gui.widget.TextFieldWidget; import net.minecraft.text.Text; @@ -9,7 +12,7 @@ import java.util.Map; public class EntryInfo { public Field field; - public Object widget; + public WidgetFactory widget; public int width; public Map.Entry error; public Object defaultValue; @@ -17,4 +20,8 @@ public class EntryInfo { public String tempValue; public boolean inLimits = true; public Entry entry; + + public interface WidgetFactory { + ClickableWidget build(int width, TextRenderer textRenderer, ButtonWidget done); + } } diff --git a/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/client/screen/EntryInfoWidgetBuilder.java b/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/client/screen/EntryInfoWidgetBuilder.java index 7a4366d..0ea1b77 100644 --- a/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/client/screen/EntryInfoWidgetBuilder.java +++ b/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/client/screen/EntryInfoWidgetBuilder.java @@ -1,5 +1,6 @@ package io.gitlab.jfronny.libjf.config.impl.client.screen; +import io.gitlab.jfronny.libjf.LibJf; import io.gitlab.jfronny.libjf.config.api.ConfigInstance; import io.gitlab.jfronny.libjf.config.api.Entry; import io.gitlab.jfronny.libjf.config.impl.EntryInfo; @@ -16,7 +17,6 @@ import net.minecraft.util.Formatting; import java.util.AbstractMap; import java.util.Arrays; import java.util.List; -import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; import java.util.regex.Pattern; @@ -34,6 +34,9 @@ public class EntryInfoWidgetBuilder { } catch (Exception ignored) { } } + for (ConfigInstance value : config.getCategories().values()) { + initConfig(value); + } } private static void initEntry(ConfigInstance config, EntryInfo info) { @@ -43,25 +46,25 @@ public class EntryInfoWidgetBuilder { if (info.entry == null) return; - if (type == int.class || type == Integer.class) textField(config, info, Integer::parseInt, INTEGER_ONLY, info.entry.min(), info.entry.max(), true); - else if (type == float.class || type == Float.class) textField(config, info, Float::parseFloat, DECIMAL_ONLY, info.entry.min(), info.entry.max(),false); - else if (type == double.class || type == Double.class) textField(config, info, Double::parseDouble, DECIMAL_ONLY, info.entry.min(), info.entry.max(),false); - else if (type == String.class) textField(config, info, String::length, null, Math.min(info.entry.min(),0), Math.max(info.entry.max(),1),true); + if (type == int.class || type == Integer.class) textField(config, info, INTEGER_ONLY, Integer::parseInt, true, info.entry.min(), info.entry.max()); + else if (type == float.class || type == Float.class) textField(config, info, DECIMAL_ONLY, Float::parseFloat, false, info.entry.min(), info.entry.max()); + else if (type == double.class || type == Double.class) textField(config, info, DECIMAL_ONLY, Double::parseDouble, false, info.entry.min(), info.entry.max()); + else if (type == String.class) textField(config, info, null, String::length, true, Math.min(info.entry.min(),0), Math.max(info.entry.max(),1)); else if (type == boolean.class || type == Boolean.class) { Function func = value -> new LiteralText((Boolean) value ? "True" : "False").formatted((Boolean) value ? Formatting.GREEN : Formatting.RED); - info.widget = new AbstractMap.SimpleEntry>(button -> { + toggle(info, button -> { info.value = !(Boolean) info.value; button.setMessage(func.apply(info.value)); }, func); } else if (type.isEnum()) { List values = Arrays.asList(info.field.getType().getEnumConstants()); Function func = value -> new TranslatableText(config.getModId() + ".jfconfig.enum." + type.getSimpleName() + "." + info.value.toString()); - info.widget = new AbstractMap.SimpleEntry>(button -> { + toggle(info, button -> { int index = values.indexOf(info.value) + 1; - info.value = values.get(index >= values.size()? 0 : index); + info.value = values.get(index >= values.size() ? 0 : index); button.setMessage(func.apply(info.value)); }, func); - } + } else LibJf.LOGGER.error("Invalid entry type in " + info.field.getName() + ": " + type.getName()); try { info.value = info.field.get(null); @@ -70,32 +73,53 @@ public class EntryInfoWidgetBuilder { } } - private static void textField(ConfigInstance config, EntryInfo info, Function f, Pattern pattern, double min, double max, boolean cast) { + private static void toggle(EntryInfo info, ButtonWidget.PressAction pressAction, Function valueTextifier) { + info.widget = (width, textRenderer, done) -> new ButtonWidget(width - 110, 0, info.width, 20, valueTextifier.apply(info.value), pressAction); + } + + /** + * @param config The config this entry is a part of + * @param info The entry to generate a widget for + * @param pattern The pattern a valid value must abide to + * @param sizeFetcher A function to get a number for size constraints + * @param wholeNumber Whether size constraints are whole numbers + * @param min The minimum size of a valid value + * @param max The maximum size of a valid value + */ + private static void textField(ConfigInstance config, EntryInfo info, Pattern pattern, Function sizeFetcher, boolean wholeNumber, double min, double max) { boolean isNumber = pattern != null; - info.widget = (BiFunction>) (t, b) -> s -> { - s = s.trim(); - if (!(s.isEmpty() || !isNumber || pattern.matcher(s).matches())) return false; + info.widget = (width, textRenderer, done) -> { + TextFieldWidget widget = new TextFieldWidget(textRenderer, width - 110, 0, info.width, 20, null); - Number value = 0; - boolean inLimits = false; - info.error = null; - if (!(isNumber && s.isEmpty()) && !s.equals("-") && !s.equals(".")) { - value = f.apply(s); - inLimits = value.doubleValue() >= min && value.doubleValue() <= max; - info.error = inLimits? null : new AbstractMap.SimpleEntry<>(t, new LiteralText(value.doubleValue() < min ? - "§cMinimum " + (isNumber? "value" : "length") + (cast? " is " + (int)min : " is " + min) : - "§cMaximum " + (isNumber? "value" : "length") + (cast? " is " + (int)max : " is " + max))); - } + widget.setText(info.tempValue); + Predicate processor = s -> { + s = s.trim(); + if (!(s.isEmpty() || !isNumber || pattern.matcher(s).matches())) return false; - info.tempValue = s; - t.setEditableColor(inLimits? 0xFFFFFFFF : 0xFFFF7777); - info.inLimits = inLimits; - b.active = config.getEntries().stream().allMatch(e -> e.inLimits); + Number value = 0; + boolean inLimits = false; + info.error = null; + if (!(isNumber && s.isEmpty()) && !s.equals("-") && !s.equals(".")) { + value = sizeFetcher.apply(s); + inLimits = value.doubleValue() >= min && value.doubleValue() <= max; + info.error = inLimits? null : new AbstractMap.SimpleEntry<>(widget, new LiteralText(value.doubleValue() < min ? + "§cMinimum " + (isNumber? "value" : "length") + (wholeNumber ? " is " + (int) min : " is " + min) : + "§cMaximum " + (isNumber? "value" : "length") + (wholeNumber ? " is " + (int) max : " is " + max))); + } - if (inLimits) - info.value = isNumber? value : s; + info.tempValue = s; + widget.setEditableColor(inLimits? 0xFFFFFFFF : 0xFFFF7777); + info.inLimits = inLimits; + done.active = config.getEntries().stream().allMatch(e -> e.inLimits); - return true; + if (inLimits) + info.value = isNumber? value : s; + + return true; + }; + widget.setTextPredicate(processor); + + return widget; }; } } diff --git a/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/client/screen/TinyConfigScreen.java b/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/client/screen/TinyConfigScreen.java index 30cc8da..3037994 100644 --- a/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/client/screen/TinyConfigScreen.java +++ b/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/client/screen/TinyConfigScreen.java @@ -10,7 +10,6 @@ import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.screen.ScreenTexts; import net.minecraft.client.gui.widget.ButtonWidget; -import net.minecraft.client.gui.widget.TextFieldWidget; import net.minecraft.client.resource.language.I18n; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.text.LiteralText; @@ -19,11 +18,7 @@ import net.minecraft.text.TranslatableText; import net.minecraft.util.Formatting; import java.util.*; -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.function.Predicate; -//TODO fix missing controls for subcategories @Environment(EnvType.CLIENT) public class TinyConfigScreen extends Screen { public TinyConfigScreen(ConfigInstance config, Screen parent) { @@ -73,25 +68,15 @@ public class TinyConfigScreen extends Screen { } for (EntryInfo info : config.getEntries()) { TranslatableText name = new TranslatableText(translationPrefix + info.field.getName()); - ButtonWidget resetButton = new ButtonWidget(width - 155, 0, 40, 20, new LiteralText("Reset").formatted(Formatting.RED), (button -> { - info.value = info.defaultValue; - info.tempValue = info.value.toString(); - double scrollAmount = list.getScrollAmount(); - Objects.requireNonNull(client).setScreen(this); - list.setScrollAmount(scrollAmount); - })); - - if (info.widget instanceof Map.Entry) { - Map.Entry> widget = (Map.Entry>) info.widget; - if (info.field.getType().isEnum()) widget.setValue(value -> new TranslatableText(translationPrefix + "enum." + info.field.getType().getSimpleName() + "." + info.value.toString())); - this.list.addButton(new ButtonWidget(width - 110, 0, info.width, 20, widget.getValue().apply(info.value), widget.getKey()),resetButton,name); - } else if (info.widget != null) { - TextFieldWidget widget = new TextFieldWidget(textRenderer, width - 110, 0, info.width, 20, null); - - widget.setText(info.tempValue); - Predicate processor = ((BiFunction>) info.widget).apply(widget, done); - widget.setTextPredicate(processor); - this.list.addButton(widget, resetButton, name); + if (info.widget != null) { + ButtonWidget resetButton = new ButtonWidget(width - 155, 0, 40, 20, new LiteralText("Reset").formatted(Formatting.RED), (button -> { + info.value = info.defaultValue; + info.tempValue = info.value.toString(); + double scrollAmount = list.getScrollAmount(); + Objects.requireNonNull(client).setScreen(this); + list.setScrollAmount(scrollAmount); + })); + this.list.addButton(info.widget.build(width, textRenderer, done), resetButton, name); } else { ButtonWidget dummy = new ButtonWidget(-10, 0, 0, 0, Text.of(""), null); this.list.addButton(dummy,dummy,name);