Add verifiers and presets to JfConfig

This commit is contained in:
Johannes Frohnmeyer 2022-01-08 20:02:15 +01:00
parent 7b43af0450
commit 3fb16789b9
Signed by: Johannes
GPG Key ID: E76429612C2929F4
20 changed files with 349 additions and 40 deletions

View File

@ -31,8 +31,40 @@ To register a config, add a `libjf:config` entrypoint pointing to its class to y
To manually register a config or save changes, use `io.gitlab.jfronny.libjf.config.api.ConfigHolder`
For example, to save a config for a mod titled `yourmod`:
```java
ConfigHolder.getInstance().getRegistered().get("yourmod").write();
ConfigHolder.getInstance().get("yourmod").write();
```
LibJF config is only intended for simple config screens, it does not support nested classes, multiple pages or controls like sliders.
Use something else for those
Use something else for those
## Presets
libjf-config-v0 provides a preset system to automatically fill in certain values based on a function.
To add a snippet, add a public static method to your config class and annotate it with @Preset.
If your preset is selected, the method will be executed.
You may assign a name by using your language file, the format for names is `<mod id>.jfconfig.<method name>`
Example:
```java
@Preset
public static void moskau() {
disablePacks = true;
disablePacks2 = true;
intTest = -5;
floatTest = -6;
doubleTest = 4;
dieStr = "Moskau";
}
```
## Verifiers
If you need to manually validate config values outside of minimums or maximums, you may add a public static method
and annotate it with @Verifier. This method will be executed whenever your config changes, which might happen often.
Be careful to write performant code here!
Example:
```java
@Verifier
public static void setIntTestIfDisable() {
if (disablePacks) intTest = 0;
}
```

View File

@ -1,6 +1,7 @@
# libjf-devutil-v0
LibJF devutil is intended to be used as `runtimeOnly`.
It marks the running minecraft instance as a development instance and disables the UserApi (removing that Yggdrasil error message)
It marks the running minecraft instance as a development instance (for example, this removes the need for eula.txt)
and disables the UserApi (removing that Yggdrasil error message)
It does not provide any useful functionality to end users
It depends on libjf-base

View File

@ -11,5 +11,7 @@ public interface ConfigHolder {
}
void register(String modId, Class<?> config);
Map<String, Config> getRegistered();
Config get(Class<?> configClass);
Config get(String configClass);
boolean isRegistered(Class<?> config);
}

View File

@ -0,0 +1,11 @@
package io.gitlab.jfronny.libjf.config.api;
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.METHOD)
public @interface Preset {
}

View File

@ -0,0 +1,11 @@
package io.gitlab.jfronny.libjf.config.api;
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.METHOD)
public @interface Verifier {
}

View File

@ -2,20 +2,27 @@ package io.gitlab.jfronny.libjf.config.impl;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.config.api.Entry;
import io.gitlab.jfronny.libjf.config.api.Preset;
import io.gitlab.jfronny.libjf.config.api.Verifier;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.util.math.MathHelper;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
import java.util.logging.Logger;
/** Based on https://github.com/TeamMidnightDust/MidnightLib which is based on https://github.com/Minenash/TinyConfig
* Credits to TeamMidnightDust and Minenash */
public class Config {
public final List<EntryInfo> entries = new ArrayList<>();
public Path path;
public final Map<String, Runnable> presets = new LinkedHashMap<>();
public final Set<Runnable> verifiers = new LinkedHashSet<>();
public final Path path;
public final String modid;
public final Class<?> configClass;
@ -27,19 +34,106 @@ public class Config {
for (Field field : config.getFields()) {
EntryInfo info = new EntryInfo();
info.field = field;
if (field.isAnnotationPresent(Entry.class))
if (field.isAnnotationPresent(Entry.class)) {
info.entry = field.getAnnotation(Entry.class);
try {
info.defaultValue = field.get(null);
} catch (IllegalAccessException ignored) {}
}
entries.add(info);
}
presets.put("libjf-config-v0.default", () -> {
for (EntryInfo entry : entries) {
try {
entry.field.set(null, entry.defaultValue);
} catch (IllegalAccessException e) {
LibJf.LOGGER.error("Could not reload default values", e);
}
}
});
for (Method method : config.getMethods()) {
if (method.isAnnotationPresent(Preset.class)) {
presets.put(modid + ".jfconfig." + method.getName(), () -> {
try {
method.invoke(null);
} catch (IllegalAccessException | InvocationTargetException e) {
LibJf.LOGGER.error("Could not apply preset", e);
}
});
}
else if (method.isAnnotationPresent(Verifier.class)) {
verifiers.add(() -> {
try {
method.invoke(null);
} catch (IllegalAccessException | InvocationTargetException e) {
LibJf.LOGGER.error("Could not run verifier", e);
}
});
}
}
verifiers.add(() -> {
for (EntryInfo entry : entries) {
Object value;
try {
value = entry.field.get(null);
} catch (IllegalAccessException e) {
LibJf.LOGGER.error("Could not read value", e);
continue;
}
final Object valueOriginal = value;
if (value instanceof final Integer v) {
if (v < entry.entry.min()) value = (int)entry.entry.min();
if (v > entry.entry.max()) value = (int)entry.entry.max();
} else if (value instanceof final Float v) {
if (v < entry.entry.min()) value = (float)entry.entry.min();
if (v > entry.entry.max()) value = (float)entry.entry.max();
} else if (value instanceof final Double v) {
if (v < entry.entry.min()) value = entry.entry.min();
if (v > entry.entry.max()) value = entry.entry.max();
}
if (valueOriginal != value) {
try {
entry.field.set(null, value);
} catch (IllegalAccessException e) {
LibJf.LOGGER.error("Could not write value", e);
}
}
}
});
try {
LibJf.GSON.fromJson(Files.newBufferedReader(path), config); }
catch (Exception e) { write(); }
LibJf.GSON.fromJson(Files.newBufferedReader(path), config);
syncFromClass();
}
catch (Exception e) {
write();
}
}
public void syncToClass() {
for (EntryInfo info : entries) {
try {
info.field.set(null, info.value);
} catch (IllegalAccessException e) {
LibJf.LOGGER.error("Could not write value", e);
}
}
syncFromClass();
}
public void syncFromClass() {
for (Runnable verifier : verifiers) {
verifier.run();
}
for (EntryInfo info : entries) {
try {
info.value = info.field.get(null);
} catch (IllegalAccessException e) {
LibJf.LOGGER.error("Could not read value", e);
}
}
}
public void write() {
path = FabricLoader.getInstance().getConfigDir().resolve(modid + ".json");
try {
if (!Files.exists(path)) Files.createFile(path);
Files.write(path, LibJf.GSON.toJson(configClass.getDeclaredConstructor().newInstance()).getBytes());

View File

@ -2,12 +2,14 @@ package io.gitlab.jfronny.libjf.config.impl;
import com.google.common.collect.ImmutableMap;
import io.gitlab.jfronny.libjf.config.api.ConfigHolder;
import org.jetbrains.annotations.ApiStatus;
import java.util.HashMap;
import java.util.Map;
public class ConfigHolderImpl implements ConfigHolder {
@Deprecated public static final ConfigHolderImpl INSTANCE = new ConfigHolderImpl();
@ApiStatus.Internal
public static final ConfigHolderImpl INSTANCE = new ConfigHolderImpl();
private ConfigHolderImpl() {}
private final Map<String, Config> configs = new HashMap<>();
@ -22,6 +24,20 @@ public class ConfigHolderImpl implements ConfigHolder {
return ImmutableMap.copyOf(configs);
}
@Override
public Config get(Class<?> configClass) {
for (Config value : configs.values()) {
if (value.configClass.equals(configClass))
return value;
}
return null;
}
@Override
public Config get(String configClass) {
return configs.get(configClass);
}
public boolean isRegistered(Class<?> config) {
for (Config value : configs.values()) {
if (value.configClass.equals(config))

View File

@ -1,5 +1,6 @@
package io.gitlab.jfronny.libjf.config.impl;
import io.gitlab.jfronny.libjf.config.api.Entry;
import net.minecraft.client.gui.widget.TextFieldWidget;
import net.minecraft.text.Text;
@ -15,4 +16,5 @@ public class EntryInfo {
public Object value;
public String tempValue;
public boolean inLimits = true;
public Entry entry;
}

View File

@ -18,7 +18,7 @@ import java.util.List;
import java.util.Map;
@Environment(EnvType.CLIENT)
public class ButtonEntry extends ElementListWidget.Entry<ButtonEntry> {
public class ConfigScreenEntry extends ElementListWidget.Entry<ConfigScreenEntry> {
private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer;
public final List<ClickableWidget> buttons = new ArrayList<>();
private final List<ClickableWidget> resetButtons = new ArrayList<>();
@ -26,7 +26,7 @@ public class ButtonEntry extends ElementListWidget.Entry<ButtonEntry> {
private final List<ClickableWidget> buttonsWithResetButtons = new ArrayList<>();
public static final Map<ClickableWidget, Text> buttonsWithText = new HashMap<>();
private ButtonEntry(ClickableWidget button, Text text, ClickableWidget resetButton) {
private ConfigScreenEntry(ClickableWidget button, Text text, ClickableWidget resetButton) {
buttonsWithText.put(button,text);
this.buttons.add(button);
this.resetButtons.add(resetButton);
@ -34,8 +34,8 @@ public class ButtonEntry extends ElementListWidget.Entry<ButtonEntry> {
this.buttonsWithResetButtons.add(button);
this.buttonsWithResetButtons.add(resetButton);
}
public static ButtonEntry create(ClickableWidget button, Text text, ClickableWidget resetButton) {
return new ButtonEntry(button, text, resetButton);
public static ConfigScreenEntry create(ClickableWidget button, Text text, ClickableWidget resetButton) {
return new ConfigScreenEntry(button, text, resetButton);
}
public void render(MatrixStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) {
this.buttons.forEach((button) -> {

View File

@ -39,15 +39,14 @@ public class EntryInfoWidgetBuilder {
private static void initEntry(Config config, EntryInfo info) {
if (!(info.field.isAnnotationPresent(io.gitlab.jfronny.libjf.config.api.Entry.class) || info.field.isAnnotationPresent(GsonHidden.class))) return;
Class<?> type = info.field.getType();
io.gitlab.jfronny.libjf.config.api.Entry e = info.field.getAnnotation(Entry.class);
info.width = e != null ? e.width() : 0;
info.width = info.entry != null ? info.entry.width() : 0;
if (e == null) return;
if (info.entry == null) return;
if (type == int.class || type == Integer.class) textField(config, info, Integer::parseInt, INTEGER_ONLY, e.min(), e.max(), true);
else if (type == float.class || type == Float.class) textField(config, info, Float::parseFloat, DECIMAL_ONLY, e.min(), e.max(),false);
else if (type == double.class || type == Double.class) textField(config, info, Double::parseDouble, DECIMAL_ONLY, e.min(), e.max(),false);
else if (type == String.class) textField(config, info, String::length, null, Math.min(e.min(),0), Math.max(e.max(),1),true);
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);
else if (type == boolean.class || type == Boolean.class) {
Function<Object, Text> func = value -> new LiteralText((Boolean) value ? "True" : "False").formatted((Boolean) value ? Formatting.GREEN : Formatting.RED);
info.widget = new AbstractMap.SimpleEntry<ButtonWidget.PressAction, Function<Object, Text>>(button -> {

View File

@ -11,7 +11,7 @@ import net.minecraft.text.Text;
import java.util.Optional;
@Environment(EnvType.CLIENT)
public class MidnightConfigListWidget extends ElementListWidget<ButtonEntry> {
public class MidnightConfigListWidget extends ElementListWidget<ConfigScreenEntry> {
TextRenderer textRenderer;
public MidnightConfigListWidget(MinecraftClient minecraftClient, int i, int j, int k, int l, int m) {
@ -23,12 +23,13 @@ public class MidnightConfigListWidget extends ElementListWidget<ButtonEntry> {
public int getScrollbarPositionX() { return this.width -7; }
public void addButton(ClickableWidget button, ClickableWidget resetButton, Text text) {
this.addEntry(ButtonEntry.create(button, text, resetButton));
this.addEntry(ConfigScreenEntry.create(button, text, resetButton));
}
@Override
public int getRowWidth() { return 10000; }
public Optional<ClickableWidget> getHoveredButton(double mouseY) {
for (ButtonEntry buttonEntry : this.children()) {
for (ConfigScreenEntry buttonEntry : this.children()) {
for (ClickableWidget button : buttonEntry.buttons) {
if (button.visible && mouseY >= button.y && mouseY < button.y + itemHeight) {
return Optional.of(button);

View File

@ -4,8 +4,10 @@ import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.config.impl.Config;
import io.gitlab.jfronny.libjf.config.api.Entry;
import io.gitlab.jfronny.libjf.config.impl.EntryInfo;
import io.gitlab.jfronny.libjf.config.impl.gui.presets.PresetsScreen;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
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;
@ -40,15 +42,19 @@ public class TinyConfigScreen extends Screen {
// Real Time config update //
@Override
public void tick() {
for (EntryInfo info : config.entries)
try { info.field.set(null, info.value); }
catch (IllegalAccessException ignored) {}
config.syncToClass();
}
@Override
protected void init() {
super.init();
config.syncFromClass();
this.addDrawableChild(new ButtonWidget(4, 6, 80, 20, new TranslatableText("libjf-config-v0.presets"), button -> {
MinecraftClient.getInstance().setScreen(new PresetsScreen(this, config));
}));
this.addDrawableChild(new ButtonWidget(this.width / 2 - 154, this.height - 28, 150, 20, ScreenTexts.CANCEL, button -> {
try {
LibJf.GSON.fromJson(Files.newBufferedReader(config.path), config.configClass); }
@ -67,10 +73,7 @@ public class TinyConfigScreen extends Screen {
}));
ButtonWidget done = this.addDrawableChild(new ButtonWidget(this.width / 2 + 4, this.height - 28, 150, 20, ScreenTexts.DONE, (button) -> {
for (EntryInfo info : config.entries)
try {
info.field.set(null, info.value);
} catch (IllegalAccessException ignored) {}
config.syncToClass();
config.write();
Objects.requireNonNull(client).setScreen(parent);
}));
@ -103,8 +106,8 @@ public class TinyConfigScreen extends Screen {
this.list.addButton(dummy,dummy,name);
}
}
}
@Override
public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) {
this.renderBackground(matrices);
@ -116,7 +119,7 @@ public class TinyConfigScreen extends Screen {
if (widget.isPresent()) {
for (EntryInfo info : config.entries) {
ClickableWidget buttonWidget = widget.get();
Text text = ButtonEntry.buttonsWithText.get(buttonWidget);
Text text = ConfigScreenEntry.buttonsWithText.get(buttonWidget);
TranslatableText name = new TranslatableText(this.translationPrefix + info.field.getName());
boolean showTooltip = text.equals(name);
String tooltipKey = translationPrefix + info.field.getName() + ".tooltip";
@ -135,4 +138,9 @@ public class TinyConfigScreen extends Screen {
}
super.render(matrices,mouseX,mouseY,delta);
}
@Override
public void onClose() {
MinecraftClient.getInstance().setScreen(parent);
}
}

View File

@ -0,0 +1,32 @@
package io.gitlab.jfronny.libjf.config.impl.gui.presets;
import net.minecraft.client.gui.Element;
import net.minecraft.client.gui.Selectable;
import net.minecraft.client.gui.widget.ClickableWidget;
import net.minecraft.client.gui.widget.ElementListWidget;
import net.minecraft.client.util.math.MatrixStack;
import java.util.List;
public class PresetEntry extends ElementListWidget.Entry<PresetEntry> {
private final ClickableWidget button;
public PresetEntry(ClickableWidget button) {
this.button = button;
}
@Override
public List<? extends Selectable> selectableChildren() {
return List.of(button);
}
@Override
public List<? extends Element> children() {
return List.of(button);
}
@Override
public void render(MatrixStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) {
button.y = y;
button.render(matrices, mouseX, mouseY, tickDelta);
}
}

View File

@ -0,0 +1,20 @@
package io.gitlab.jfronny.libjf.config.impl.gui.presets;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.widget.ClickableWidget;
import net.minecraft.client.gui.widget.ElementListWidget;
public class PresetListWidget extends ElementListWidget<PresetEntry> {
public PresetListWidget(MinecraftClient client, int i, int j, int k, int l, int m) {
super(client, i, j, k, l, m);
}
public void addButton(ClickableWidget button) {
addEntry(new PresetEntry(button));
}
@Override
public int getScrollbarPositionX() { return this.width -7; }
@Override
public int getRowWidth() { return 10000; }
}

View File

@ -0,0 +1,58 @@
package io.gitlab.jfronny.libjf.config.impl.gui.presets;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.config.impl.Config;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.text.TranslatableText;
import java.util.Map;
@Environment(EnvType.CLIENT)
public class PresetsScreen extends Screen {
private final Screen parent;
private final Config config;
private PresetListWidget list;
public PresetsScreen(Screen parent, Config config) {
super(new TranslatableText("libjf-config-v0.presets"));
this.parent = parent;
this.config = config;
}
@Override
protected void init() {
super.init();
this.list = new PresetListWidget(this.client, this.width, this.height, 32, this.height - 32, 25);
for (Map.Entry<String, Runnable> entry : config.presets.entrySet()) {
this.list.addButton(new ButtonWidget(width / 2 - 100, 0, 200, 20,
new TranslatableText(entry.getKey()),
button -> {
LibJf.LOGGER.info("Preset selected: " + entry.getKey());
entry.getValue().run();
config.syncFromClass();
MinecraftClient.getInstance().setScreen(parent);
}));
}
this.addSelectableChild(this.list);
}
@Override
public void onClose() {
MinecraftClient.getInstance().setScreen(parent);
}
@Override
public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) {
this.renderBackground(matrices);
this.list.render(matrices, mouseX, mouseY, delta);
drawCenteredText(matrices, textRenderer, title, width / 2, 15, 0xFFFFFF);
super.render(matrices, mouseX, mouseY, delta);
}
}

View File

@ -0,0 +1,4 @@
{
"libjf-config-v0.presets": "Presets",
"libjf-config-v0.default": "Default"
}

View File

@ -2,6 +2,8 @@ package io.gitlab.jfronny.libjf.config.test;
import io.gitlab.jfronny.libjf.config.api.JfConfig;
import io.gitlab.jfronny.libjf.config.api.Entry;
import io.gitlab.jfronny.libjf.config.api.Preset;
import io.gitlab.jfronny.libjf.config.api.Verifier;
import io.gitlab.jfronny.libjf.gson.GsonHidden;
public class TestConfig implements JfConfig {
@ -15,6 +17,21 @@ public class TestConfig implements JfConfig {
public static String gsonOnlyStr = "lolz";
@Entry public static Test enumTest = Test.Test;
@Preset
public static void moskau() {
disablePacks = true;
disablePacks2 = true;
intTest = -5;
floatTest = -6;
doubleTest = 4;
dieStr = "Moskau";
}
@Verifier
public static void setIntTestIfDisable() {
if (disablePacks) intTest = 0;
}
public enum Test {
Test, ER
}

View File

@ -8,5 +8,6 @@
"libjf-config-v0-testmod.jfconfig.enumTest": "Enum Test",
"libjf-config-v0-testmod.jfconfig.enumTest.tooltip": "Enum Test Tooltip",
"libjf-config-v0-testmod.jfconfig.enum.Test.Test": "Test",
"libjf-config-v0-testmod.jfconfig.enum.Test.ER": "ER"
"libjf-config-v0-testmod.jfconfig.enum.Test.ER": "ER",
"libjf-config-v0-testmod.jfconfig.moskau": "Moskau"
}

View File

@ -39,7 +39,7 @@ public class UserResourceEvents {
public static final Event<Contains> CONTAINS = EventFactory.createArrayBacked(Contains.class,
(listeners) -> (type, id, previous, pack) -> {
LazySupplier<Boolean> lazy = new LazySupplier<>(previous); //TODO make initial value lazy properly
LazySupplier<Boolean> lazy = new LazySupplier<>(previous);
for (Contains listener : listeners) {
lazy = lazy.andThen(supplier -> listener.contains(type, id, supplier, pack));
}
@ -48,7 +48,7 @@ public class UserResourceEvents {
public static final Event<FindResource> FIND_RESOURCE = EventFactory.createArrayBacked(FindResource.class,
(listeners) -> ((type, namespace, prefix, maxDepth, pathFilter, previous, pack) -> {
LazySupplier<Collection<Identifier>> lazy = new LazySupplier<>(previous); //TODO make initial value lazy properly
LazySupplier<Collection<Identifier>> lazy = new LazySupplier<>(previous);
for (FindResource listener : listeners) {
lazy = lazy.andThen(supplier -> listener.findResources(type, namespace, prefix, maxDepth, pathFilter, supplier, pack));
}
@ -57,7 +57,7 @@ public class UserResourceEvents {
public static final Event<Open> OPEN = EventFactory.createArrayBacked(Open.class,
(listeners) -> ((type, id, previous, pack) -> {
LazySupplier<InputStream> lazy = new LazySupplier<>(previous); //TODO make initial value lazy properly
LazySupplier<InputStream> lazy = new LazySupplier<>(previous);
for (Open listener : listeners) {
lazy = lazy.andThen(supplier -> {
try {
@ -73,7 +73,7 @@ public class UserResourceEvents {
public static final Event<OpenRoot> OPEN_ROOT = EventFactory.createArrayBacked(OpenRoot.class,
(listeners) -> ((fileName, previous, pack) -> {
LazySupplier<InputStream> lazy = new LazySupplier<>(previous); //TODO make initial value lazy properly
LazySupplier<InputStream> lazy = new LazySupplier<>(previous);
for (OpenRoot listener : listeners) {
lazy = lazy.andThen(supplier -> {
try {

View File

@ -4,9 +4,9 @@
"package": "io.gitlab.jfronny.libjf.devutil.mixin",
"compatibilityLevel": "JAVA_16",
"mixins": [
"MinecraftClientMixin"
],
"client": [
"MinecraftClientMixin"
],
"injectors": {
"defaultRequire": 1