feat: conditions! + large cleanup

This commit is contained in:
Martin Prokoph
2025-03-27 23:30:03 +01:00
parent 465af9507e
commit 2abf9040a7
3 changed files with 95 additions and 32 deletions

View File

@@ -40,27 +40,34 @@ public abstract class MidnightConfig {
private static final Pattern HEXADECIMAL_ONLY = Pattern.compile("(-?[#0-9a-fA-F]*)"); private static final Pattern HEXADECIMAL_ONLY = Pattern.compile("(-?[#0-9a-fA-F]*)");
private static final List<EntryInfo> entries = new ArrayList<>(); private static final List<EntryInfo> entries = new ArrayList<>();
private static boolean reloadScreen = false;
public static class EntryInfo { public static class EntryInfo {
public Entry entry; public Entry entry;
public Comment comment; public Comment comment;
final Field field; public Condition condition;
Class<?> dataType; public final Field field;
public final Class<?> dataType;
public final String modid, fieldName;
int listIndex; int listIndex;
Object defaultValue, value, function; Object defaultValue, value, function;
String modid, fieldName, tempValue = ""; // The value visible in the config screen String tempValue; // The value visible in the config screen
boolean inLimits = true; boolean inLimits = true;
Text name, error; Text name, error;
ClickableWidget actionButton; // color picker button / explorer button ClickableWidget actionButton; // color picker button / explorer button
Tab tab; Tab tab;
boolean conditionsMet = true;
public EntryInfo(Field field) { public EntryInfo(Field field, String modid) {
this.field = field; this.field = field; this.modid = modid;
if (field != null) { if (field != null) {
this.fieldName = field.getName(); this.fieldName = field.getName(); this.dataType = getUnderlyingType(field);
this.entry = field.getAnnotation(Entry.class); this.entry = field.getAnnotation(Entry.class); this.comment = field.getAnnotation(Comment.class); this.condition = field.getAnnotation(Condition.class);
this.comment = field.getAnnotation(Comment.class); } else {
this.fieldName = ""; this.dataType = null;
} }
if (entry != null && !entry.name().isEmpty()) this.name = Text.translatable(entry.name());
else if (comment != null && !comment.name().isEmpty()) this.name = Text.translatable(comment.name());
} }
public void setValue(Object value) { public void setValue(Object value) {
if (this.field.getType() != List.class) { this.value = value; if (this.field.getType() != List.class) { this.value = value;
@@ -73,7 +80,19 @@ public abstract class MidnightConfig {
else try { return ((List<?>) this.value).get(this.listIndex).toString(); } catch (Exception ignored) {return "";} else try { return ((List<?>) this.value).get(this.listIndex).toString(); } catch (Exception ignored) {return "";}
} }
public void updateFieldValue() { public void updateFieldValue() {
try { this.field.set(null, this.value); } catch (IllegalAccessException ignored) {} try { if (this.field.get(null) != value) updateConditions(tempValue);
this.field.set(null, this.value);
} catch (IllegalAccessException ignored) {}
}
@SuppressWarnings("ConstantValue") //pertains to requiredModLoaded
public void updateConditions(String newTempValue) {
for (EntryInfo info : entries) {
boolean prevConditionState = info.conditionsMet;
if (info.condition != null && ((info.condition.requiredOption().contains(":") ? "" : info.modid + ":") + info.condition.requiredOption()).equals(this.modid + ":" + this.fieldName))
info.conditionsMet = Objects.equals(info.condition.requiredValue(), newTempValue);
if (info.condition != null && !info.condition.requiredModId().isEmpty() && !PlatformFunctions.isModLoaded(info.condition.requiredModId())) info.conditionsMet = false;
if (prevConditionState != info.conditionsMet) reloadScreen = true;
}
} }
public <T> void writeList(int index, T value) { public <T> void writeList(int index, T value) {
var list = (List<T>) this.value; var list = (List<T>) this.value;
@@ -105,6 +124,7 @@ public abstract class MidnightConfig {
for (EntryInfo info : entries) if (info.field != null && info.entry != null) { for (EntryInfo info : entries) if (info.field != null && info.entry != null) {
try { info.value = info.field.get(null); info.tempValue = info.toTemporaryValue(); try { info.value = info.field.get(null); info.tempValue = info.toTemporaryValue();
info.updateConditions(info.tempValue);
} catch (IllegalAccessException ignored) {} } catch (IllegalAccessException ignored) {}
} }
} }
@@ -113,7 +133,7 @@ public abstract class MidnightConfig {
configClass.put(modid, config); configClass.put(modid, config);
for (Field field : config.getFields()) { for (Field field : config.getFields()) {
EntryInfo info = new EntryInfo(field); EntryInfo info = new EntryInfo(field, modid);
if ((field.isAnnotationPresent(Entry.class) || field.isAnnotationPresent(Comment.class)) && !field.isAnnotationPresent(Server.class) && !field.isAnnotationPresent(Hidden.class) && PlatformFunctions.isClientEnv()) if ((field.isAnnotationPresent(Entry.class) || field.isAnnotationPresent(Comment.class)) && !field.isAnnotationPresent(Server.class) && !field.isAnnotationPresent(Hidden.class) && PlatformFunctions.isClientEnv())
initClient(modid, field, info); initClient(modid, field, info);
if (field.isAnnotationPresent(Entry.class)) if (field.isAnnotationPresent(Entry.class))
@@ -122,19 +142,10 @@ public abstract class MidnightConfig {
} }
loadValuesFromJson(modid); loadValuesFromJson(modid);
} }
@SuppressWarnings("ConstantValue") //pertains to requiredModLoaded
@Environment(EnvType.CLIENT) @Environment(EnvType.CLIENT)
private static void initClient(String modid, Field field, EntryInfo info) { private static void initClient(String modid, Field field, EntryInfo info) {
info.dataType = getUnderlyingType(field); Entry e = info.entry;
Entry e = info.entry; Comment c = info.comment;
info.modid = modid;
boolean requiredModLoaded = true;
if (e != null) { if (e != null) {
if (!e.requiredMod().isEmpty()) requiredModLoaded = PlatformFunctions.isModLoaded(e.requiredMod());
if (!requiredModLoaded) return;
if (!e.name().isEmpty()) info.name = Text.translatable(e.name());
if (info.dataType == int.class) textField(info, Integer::parseInt, INTEGER_ONLY, (int) e.min(), (int) e.max(), true); if (info.dataType == int.class) textField(info, Integer::parseInt, INTEGER_ONLY, (int) e.min(), (int) e.max(), true);
else if (info.dataType == float.class) textField(info, Float::parseFloat, DECIMAL_ONLY, (float) e.min(), (float) e.max(), false); else if (info.dataType == float.class) textField(info, Float::parseFloat, DECIMAL_ONLY, (float) e.min(), (float) e.max(), false);
else if (info.dataType == double.class) textField(info, Double::parseDouble, DECIMAL_ONLY, e.min(), e.max(), false); else if (info.dataType == double.class) textField(info, Double::parseDouble, DECIMAL_ONLY, e.min(), e.max(), false);
@@ -154,10 +165,9 @@ public abstract class MidnightConfig {
int index = values.indexOf(info.value) + 1; int index = values.indexOf(info.value) + 1;
info.value = values.get(index >= values.size() ? 0 : index); button.setMessage(func.apply(info.value)); info.value = values.get(index >= values.size() ? 0 : index); button.setMessage(func.apply(info.value));
}, func); }, func);
}} else if (c != null) { }
if (!c.requiredMod().isEmpty()) requiredModLoaded = PlatformFunctions.isModLoaded(c.requiredMod());
} }
if (requiredModLoaded) entries.add(info); entries.add(info);
} }
public static Class<?> getUnderlyingType(Field field) { public static Class<?> getUnderlyingType(Field field) {
Class<?> rawType = field.getType(); Class<?> rawType = field.getType();
@@ -259,12 +269,12 @@ public abstract class MidnightConfig {
super.tick(); super.tick();
if (prevTab != null && prevTab != tabManager.getCurrentTab()) { if (prevTab != null && prevTab != tabManager.getCurrentTab()) {
prevTab = tabManager.getCurrentTab(); prevTab = tabManager.getCurrentTab();
this.list.clear(); fillList(); updateList(); list.setScrollY(0);
list.setScrollY(0);
} }
scrollProgress = list.getScrollY(); scrollProgress = list.getScrollY();
for (EntryInfo info : entries) info.updateFieldValue(); for (EntryInfo info : entries) info.updateFieldValue();
updateButtons(); updateButtons();
if (reloadScreen) { updateList(); reloadScreen = false; }
} }
public void updateButtons() { public void updateButtons() {
if (this.list != null) { if (this.list != null) {
@@ -307,14 +317,18 @@ public abstract class MidnightConfig {
this.addSelectableChild(this.list); fillList(); this.addSelectableChild(this.list); fillList();
if (tabs.size() > 1) list.renderHeaderSeparator = false; if (tabs.size() > 1) list.renderHeaderSeparator = false;
} }
public void updateList() {
this.list.clear(); fillList();
}
public void fillList() { public void fillList() {
for (EntryInfo info : entries) { for (EntryInfo info : entries) {
if (!info.conditionsMet && info.condition != null && !info.condition.visibleButLocked()) continue;
if (info.modid.equals(modid) && (info.tab == null || info.tab == tabManager.getCurrentTab())) { if (info.modid.equals(modid) && (info.tab == null || info.tab == tabManager.getCurrentTab())) {
Text name = Objects.requireNonNullElseGet(info.name, () -> Text.translatable(translationPrefix + info.fieldName)); Text name = Objects.requireNonNullElseGet(info.name, () -> Text.translatable(translationPrefix + info.fieldName));
TextIconButtonWidget resetButton = TextIconButtonWidget.builder(Text.translatable("controls.reset"), (button -> { TextIconButtonWidget resetButton = TextIconButtonWidget.builder(Text.translatable("controls.reset"), (button -> {
info.value = info.defaultValue; info.listIndex = 0; info.value = info.defaultValue; info.listIndex = 0;
info.tempValue = info.toTemporaryValue(); info.tempValue = info.toTemporaryValue();
list.clear(); fillList(); updateList();
}), true).texture(Identifier.of("midnightlib","icon/reset"), 12, 12).dimension(20, 20).build(); }), true).texture(Identifier.of("midnightlib","icon/reset"), 12, 12).dimension(20, 20).build();
resetButton.setPosition(width - 205 + 150 + 25, 0); resetButton.setPosition(width - 205 + 150 + 25, 0);
@@ -348,7 +362,7 @@ public abstract class MidnightConfig {
if (info.listIndex > values.size()) info.listIndex = 0; if (info.listIndex > values.size()) info.listIndex = 0;
info.tempValue = info.toTemporaryValue(); info.tempValue = info.toTemporaryValue();
if (info.listIndex == values.size()) info.tempValue = ""; if (info.listIndex == values.size()) info.tempValue = "";
list.clear(); fillList(); updateList();
})).dimensions(width - 185, 0, 20, 20).build(); })).dimensions(width - 185, 0, 20, 20).build();
} }
if (e.isColor()) { if (e.isColor()) {
@@ -357,7 +371,7 @@ public abstract class MidnightConfig {
Color newColor = JColorChooser.showDialog(null, Text.translatable("midnightconfig.colorChooser.title").getString(), Color.decode(!Objects.equals(info.tempValue, "") ? info.tempValue : "#FFFFFF")); Color newColor = JColorChooser.showDialog(null, Text.translatable("midnightconfig.colorChooser.title").getString(), Color.decode(!Objects.equals(info.tempValue, "") ? info.tempValue : "#FFFFFF"));
if (newColor != null) { if (newColor != null) {
info.setValue("#" + Integer.toHexString(newColor.getRGB()).substring(2)); info.setValue("#" + Integer.toHexString(newColor.getRGB()).substring(2));
list.clear(); fillList(); updateList();
} }
}).start() }).start()
).dimensions(width - 185, 0, 20, 20).build(); ).dimensions(width - 185, 0, 20, 20).build();
@@ -375,13 +389,14 @@ public abstract class MidnightConfig {
Text.translatable(translationPrefix + info.fieldName + ".fileFilter").getString(), e.fileExtensions())); Text.translatable(translationPrefix + info.fieldName + ".fileFilter").getString(), e.fileExtensions()));
if (fileChooser.showDialog(null, null) == JFileChooser.APPROVE_OPTION) { if (fileChooser.showDialog(null, null) == JFileChooser.APPROVE_OPTION) {
info.setValue(fileChooser.getSelectedFile().getAbsolutePath()); info.setValue(fileChooser.getSelectedFile().getAbsolutePath());
list.clear(); fillList(); updateList();
} }
}).start(), true }).start(), true
).texture(Identifier.of("midnightlib", "icon/explorer"), 12, 12).dimension(20, 20).build(); ).texture(Identifier.of("midnightlib", "icon/explorer"), 12, 12).dimension(20, 20).build();
explorerButton.setPosition(width - 185, 0); explorerButton.setPosition(width - 185, 0);
info.actionButton = explorerButton; info.actionButton = explorerButton;
} }
if (!info.conditionsMet) widget.active = false;
List<ClickableWidget> widgets = Lists.newArrayList(widget, resetButton); List<ClickableWidget> widgets = Lists.newArrayList(widget, resetButton);
if (info.actionButton != null) { if (info.actionButton != null) {
if (IS_SYSTEM_MAC) info.actionButton.active = false; if (IS_SYSTEM_MAC) info.actionButton.active = false;
@@ -516,7 +531,7 @@ public abstract class MidnightConfig {
boolean isSlider() default false; boolean isSlider() default false;
int precision() default 100; int precision() default 100;
String category() default "default"; String category() default "default";
String requiredMod() default ""; @Deprecated String requiredMod() default "";
} }
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Client {} @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Client {}
@@ -536,6 +551,25 @@ public abstract class MidnightConfig {
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Comment { @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Comment {
boolean centered() default false; boolean centered() default false;
String category() default "default"; String category() default "default";
String requiredMod() default ""; String name() default "";
@Deprecated String requiredMod() default "";
}
/**
* Condition Annotation<br>
* - <b>{@link Condition#requiredModId()}</b>: The id of a mod that is required to be loaded.<br>
* - <b>{@link Condition#requiredOption()}</b>: The {@link Field} which will be used to check the condition. Can also access options of other MidnightLib mods ("modid:optionName").<br>
* - <b>{@link Condition#requiredValue()}</b>: The value that {@link Condition#requiredOption()} should be set to for the condition to be met.<br>
* - <b>{@link Condition#visibleButLocked()}</b>: The behaviour to take when {@link Condition#requiredModId} is not loaded
* or {@link Condition#requiredOption()} returns a value that is not {@link Condition#requiredValue()}.<br>
* <code>true</code> Option is visible, but not editable<br>
* <code>false</code> Option is completely hidden
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Condition {
String requiredModId() default "";
String requiredOption() default "";
String requiredValue() default "true";
boolean visibleButLocked() default false;
} }
} }

View File

@@ -17,6 +17,7 @@ public class MidnightConfigExample extends MidnightConfig {
public static final String SLIDERS = "sliders"; public static final String SLIDERS = "sliders";
public static final String LISTS = "lists"; public static final String LISTS = "lists";
public static final String FILES = "files"; public static final String FILES = "files";
public static final String CONDITIONS = "conditions";
@Comment(category = TEXT) public static Comment text1; // Comments are rendered like an option without a button and are excluded from the config file @Comment(category = TEXT) public static Comment text1; // Comments are rendered like an option without a button and are excluded from the config file
@Comment(category = TEXT, centered = true) public static Comment text2; // Centered comments are the same as normal ones - just centered! @Comment(category = TEXT, centered = true) public static Comment text2; // Centered comments are the same as normal ones - just centered!
@@ -69,5 +70,32 @@ public class MidnightConfigExample extends MidnightConfig {
name = "I am a mf file/directory list!") name = "I am a mf file/directory list!")
public static List<String> fileOrDirectoryList = new ArrayList<>(); // Yes, that's right you can even have lists of files/directories public static List<String> fileOrDirectoryList = new ArrayList<>(); // Yes, that's right you can even have lists of files/directories
@Condition(requiredModId = "midnightlib") // Conditional options are here!
@Entry(category = CONDITIONS, name="Turn me on!")
public static boolean turnMeOn = false;
@Condition(requiredOption = "modid:turnMeOn", visibleButLocked = true)
@Entry(category = CONDITIONS, name="Turn me off!")
public static Boolean turnMeOff = true;
@Condition(requiredOption = "modid:turnMeOff", requiredValue = "false")
@Entry(category = CONDITIONS, name="Which is the best modloader?")
public static String bestModloader = "";
@Condition(requiredOption = "bestModloader", requiredValue = "Forge")
@Comment(category = CONDITIONS, name="❌ You have bad taste :(", centered = true) // Don't take this too seriously btw :)
public static Comment answerForge; // Comments can also be conditional!
@Condition(requiredOption = "bestModloader", requiredValue = "NeoForge")
@Comment(category = CONDITIONS, name="⛏ Not quite, but it's alright!", centered = true)
public static Comment answerNeoforge;
@Condition(requiredOption = "bestModloader", requiredValue = "Fabric")
@Comment(category = CONDITIONS, name="⭐ Correct! Fabric (and Quilt) are the best!", centered = true)
public static Comment answerFabric;
@Condition(requiredOption = "bestModloader", requiredValue = "Quilt")
@Comment(category = CONDITIONS, name="⭐ Correct! Quilt (and Fabric) are the best!", centered = true)
public static Comment answerQuilt;
@Condition(requiredOption = "midnightlib:config_screen_list", requiredValue = "FALSE") // Access options of other mods that are also using MidnightLib
@Comment(category = CONDITIONS) public static Comment spaceracer;
@Condition(requiredOption = "midnightlib:config_screen_list", requiredValue = "FALSE")
@Comment(category = CONDITIONS, name="You disabled MidnightLib's config screen list. Why? :(", centered = true) public static Comment why;
public static int imposter = 16777215; // - Entries without an @Entry or @Comment annotation are ignored public static int imposter = 16777215; // - Entries without an @Entry or @Comment annotation are ignored
} }

View File

@@ -20,5 +20,6 @@
"modid.midnightconfig.category.text": "Text", "modid.midnightconfig.category.text": "Text",
"modid.midnightconfig.category.sliders": "Sliders", "modid.midnightconfig.category.sliders": "Sliders",
"modid.midnightconfig.category.lists": "Lists", "modid.midnightconfig.category.lists": "Lists",
"modid.midnightconfig.category.files": "Files" "modid.midnightconfig.category.files": "Files",
"modid.midnightconfig.category.conditions": "Quiz"
} }