From faf8c199b443eb5dbcd963c465823fbe9cca75e4 Mon Sep 17 00:00:00 2001 From: Jaffe2718 Date: Wed, 2 Apr 2025 22:00:54 +0800 Subject: [PATCH 01/11] fix bug: the `@Condition` does not trigger in real-time to update the menu UI --- .../main/java/eu/midnightdust/lib/config/MidnightConfig.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java b/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java index 27021b1..79874f5 100755 --- a/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java +++ b/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java @@ -163,7 +163,9 @@ public abstract class MidnightConfig { }; info.function = new AbstractMap.SimpleEntry>(button -> { 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)); + info.setValue(values.get(index >= values.size() ? 0 : index)); + button.setMessage(func.apply(info.value)); }, func); } } From 6eceaded3a6e2f6a086e86f8c747c7ed252477ec Mon Sep 17 00:00:00 2001 From: Jaffe2718 Date: Thu, 3 Apr 2025 11:13:19 +0800 Subject: [PATCH 02/11] fix bugs: The condition update algorithm is chaotic and does not work properly when the condition values conflict new features: multi-conditions config is supported --- .../lib/config/MidnightConfig.java | 60 ++++++++++++++----- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java b/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java index 79874f5..63b263f 100755 --- a/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java +++ b/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java @@ -14,12 +14,13 @@ import net.minecraft.registry.Registries; import net.minecraft.screen.ScreenTexts; import net.minecraft.text.Style; import net.minecraft.text.Text; import net.minecraft.util.Formatting; import net.minecraft.util.Identifier; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.filechooser.FileNameExtensionFilter; import java.awt.Color; import java.io.IOException; -import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.lang.annotation.*; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; @@ -45,7 +46,8 @@ public abstract class MidnightConfig { public static class EntryInfo { public Entry entry; public Comment comment; - public Condition condition; +// @ApiStatus.Obsolete public Condition condition; + public Condition[] conditions; public final Field field; public final Class dataType; public final String modid, fieldName; @@ -61,8 +63,13 @@ public abstract class MidnightConfig { public EntryInfo(Field field, String modid) { this.field = field; this.modid = modid; if (field != null) { - this.fieldName = field.getName(); this.dataType = getUnderlyingType(field); - this.entry = field.getAnnotation(Entry.class); this.comment = field.getAnnotation(Comment.class); this.condition = field.getAnnotation(Condition.class); + this.fieldName = field.getName(); + this.dataType = getUnderlyingType(field); + this.entry = field.getAnnotation(Entry.class); + this.comment = field.getAnnotation(Comment.class); + // TODO: use multiple annotations +// this.condition = field.getAnnotation(Condition.class); + this.conditions = field.getAnnotationsByType(Condition.class); } else { this.fieldName = ""; this.dataType = null; } @@ -80,19 +87,30 @@ public abstract class MidnightConfig { else try { return ((List) this.value).get(this.listIndex).toString(); } catch (Exception ignored) {return "";} } public void updateFieldValue() { - try { if (this.field.get(null) != value) updateConditions(tempValue); + try { + if (this.field.get(null) != value) entries.forEach(EntryInfo::updateConditions); 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 void updateConditions() { + boolean prevConditionState = this.conditionsMet; + if (this.conditions.length > 0) this.conditionsMet = true; // reset conditions + for (Condition condition : this.conditions) { + // TODO: redefine entries as a HashMap to optimize complexity + for (EntryInfo info : entries) { + if (((condition.requiredOption().contains(":") ? "" : (this.modid + ":")) + condition.requiredOption()).equals(info.modid + ":" + info.fieldName)) { + this.conditionsMet &= info.tempValue.equals(condition.requiredValue()); +// System.out.println(this.modid + ":" + this.fieldName + "#" + condition.requiredOption() + ": " + condition.requiredValue() + " " + info.tempValue); + } + if (!condition.requiredModId().isEmpty() && !PlatformFunctions.isModLoaded(condition.requiredModId())) { + this.conditionsMet = false; + } + if (!this.conditionsMet) break; + } + if (!this.conditionsMet) break; } + if (prevConditionState != this.conditionsMet) reloadScreen = true; } public void writeList(int index, T value) { var list = (List) this.value; @@ -124,7 +142,7 @@ public abstract class MidnightConfig { for (EntryInfo info : entries) if (info.field != null && info.entry != null) { try { info.value = info.field.get(null); info.tempValue = info.toTemporaryValue(); - info.updateConditions(info.tempValue); + info.updateConditions(); } catch (IllegalAccessException ignored) {} } } @@ -324,7 +342,14 @@ public abstract class MidnightConfig { } public void fillList() { for (EntryInfo info : entries) { - if (!info.conditionsMet && info.condition != null && !info.condition.visibleButLocked()) continue; +// if (!info.conditionsMet && info.condition != null && !info.condition.visibleButLocked()) continue; + if (!info.conditionsMet) { + boolean visibleButLocked = false; + for (Condition condition : info.conditions) { + visibleButLocked |= condition.visibleButLocked(); + } + if (!visibleButLocked) continue; + } if (info.modid.equals(modid) && (info.tab == null || info.tab == tabManager.getCurrentTab())) { Text name = Objects.requireNonNullElseGet(info.name, () -> Text.translatable(translationPrefix + info.fieldName)); TextIconButtonWidget resetButton = TextIconButtonWidget.builder(Text.translatable("controls.reset"), (button -> { @@ -567,6 +592,7 @@ public abstract class MidnightConfig { * false – Option is completely hidden */ @Retention(RetentionPolicy.RUNTIME) + @Repeatable(Conditions.class) @Target(ElementType.FIELD) public @interface Condition { String requiredModId() default ""; @@ -574,4 +600,10 @@ public abstract class MidnightConfig { String requiredValue() default "true"; boolean visibleButLocked() default false; } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + public @interface Conditions { + Condition[] value(); + } } \ No newline at end of file From c1299c219522f929fa2eb72a08e44947cf290583 Mon Sep 17 00:00:00 2001 From: Jaffe2718 Date: Thu, 3 Apr 2025 11:14:02 +0800 Subject: [PATCH 03/11] add examples --- .../example/config/MidnightConfigExample.java | 38 +++++++++++++++++-- .../resources/assets/modid/lang/en_us.json | 20 +++++++--- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/test-fabric/src/main/java/eu/midnightdust/fabric/example/config/MidnightConfigExample.java b/test-fabric/src/main/java/eu/midnightdust/fabric/example/config/MidnightConfigExample.java index bee921b..c0fedc6 100644 --- a/test-fabric/src/main/java/eu/midnightdust/fabric/example/config/MidnightConfigExample.java +++ b/test-fabric/src/main/java/eu/midnightdust/fabric/example/config/MidnightConfigExample.java @@ -18,6 +18,7 @@ public class MidnightConfigExample extends MidnightConfig { public static final String LISTS = "lists"; public static final String FILES = "files"; public static final String CONDITIONS = "conditions"; + public static final String MULTI_CONDITIONS = "multiConditions"; @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! @@ -27,9 +28,9 @@ public class MidnightConfigExample extends MidnightConfig { @Entry(category = TEXT) public static String name = "Hello World!"; // Example for a string option, which is in a category! @Entry(category = TEXT, width = 7, min = 7, isColor = true, name = "I am a color!") public static String titleColor = "#ffffff"; // The isColor property adds a color chooser for a hexadecimal color @Entry(category = TEXT, idMode = 0) public static Identifier id = Identifier.ofVanilla("diamond"); // Example for an identifier with matching items displayed next to it! - @Entry(category = TEXT) public static TestEnum testEnum = TestEnum.FABRIC; // Example for an enum option - public enum TestEnum { // Enums allow the user to cycle through predefined options - QUILT, FABRIC, FORGE + @Entry(category = TEXT) public static ModPlatform modPlatform = ModPlatform.FABRIC; // Example for an enum option + public enum ModPlatform { // Enums allow the user to cycle through predefined options + QUILT, FABRIC, FORGE, NEOFORGE, VANILLA } @Entry(category = NUMBERS) public static int fabric = 16777215; // Example for an int option @Entry(category = NUMBERS) public static double world = 1.4D; // Example for a double option @@ -74,7 +75,7 @@ public class MidnightConfigExample extends MidnightConfig { @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!") + @Entry(category = CONDITIONS, name="Turn me off (locked if modid:turnMeOn is false)!") public static Boolean turnMeOff = true; @Condition(requiredOption = "modid:turnMeOff", requiredValue = "false") @Entry(category = CONDITIONS, name="Which is the best modloader?") @@ -97,5 +98,34 @@ public class MidnightConfigExample extends MidnightConfig { @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; + + // Multi-Conditions are also supported! + public enum Arch {X86, X86_64, AARCH64, RISCV64} + public enum OS {WINDOWS, MAC, LINUX} + @Entry(category = MULTI_CONDITIONS, name = "Arch") public static Arch myArch = Arch.X86; + @Entry(category = MULTI_CONDITIONS, name = "OS") public static OS myOS = OS.WINDOWS; + @Entry(category = MULTI_CONDITIONS, name = "Mod Platform") public static ModPlatform myPlatform = ModPlatform.FABRIC; + + @Condition(requiredOption = "modid:myArch", requiredValue = "X86_64") + @Condition(requiredOption = "modid:myOS", requiredValue = "WINDOWS") + @Condition(requiredOption = "modid:myPlatform", requiredValue = "FABRIC") + @Comment(category = MULTI_CONDITIONS, name = "MidnightLib can be used on Windows x86_64 with Fabric!", centered = true) public static Comment x86_64_windows_fabric; + + @Condition(requiredOption = "modid:myArch", requiredValue = "X86_64") + @Condition(requiredOption = "modid:myOS", requiredValue = "LINUX") + @Condition(requiredOption = "modid:myPlatform", requiredValue = "FABRIC") + @Comment(category = MULTI_CONDITIONS, name = "MidnightLib can be used on Linux x86_64 with Fabric!", centered = true) public static Comment x86_64_linux_fabric; + + @Condition(requiredOption = "modid:myArch", requiredValue = "X86_64") + @Condition(requiredOption = "modid:myOS", requiredValue = "WINDOWS") + @Condition(requiredOption = "modid:myPlatform", requiredValue = "NEOFORGE") + @Comment(category = MULTI_CONDITIONS, name = "MidnightLib can be used on Windows x86_64 with NeoForge!", centered = true) public static Comment x86_64_windows_neoforge; + + @Condition(requiredOption = "modid:myArch", requiredValue = "X86_64") + @Condition(requiredOption = "modid:myOS", requiredValue = "LINUX") + @Condition(requiredOption = "modid:myPlatform", requiredValue = "NEOFORGE") + @Comment(category = MULTI_CONDITIONS, name = "MidnightLib can be used on Linux x86_64 with NeoForge!", centered = true) public static Comment x86_64_linux_neoforge; + + public static int imposter = 16777215; // - Entries without an @Entry or @Comment annotation are ignored } \ No newline at end of file diff --git a/test-fabric/src/main/resources/assets/modid/lang/en_us.json b/test-fabric/src/main/resources/assets/modid/lang/en_us.json index 463f00b..b014330 100644 --- a/test-fabric/src/main/resources/assets/modid/lang/en_us.json +++ b/test-fabric/src/main/resources/assets/modid/lang/en_us.json @@ -10,10 +10,19 @@ "modid.midnightconfig.showInfo":"I am a boolean", "modid.midnightconfig.hello":"I am a limited int!", "modid.midnightconfig.id":"I am an Item Identifier!", - "modid.midnightconfig.testEnum":"I am an enum!", - "modid.midnightconfig.enum.TestEnum.FORGE":"Slow", - "modid.midnightconfig.enum.TestEnum.FABRIC":"Fancy", - "modid.midnightconfig.enum.TestEnum.QUILT":"Fabulous", + "modid.midnightconfig.modPlatform":"I am an enum!", + "modid.midnightconfig.enum.Arch.X86":"X86", + "modid.midnightconfig.enum.Arch.X86_64":"X86_64", + "modid.midnightconfig.enum.Arch.AARCH64":"AARCH64", + "modid.midnightconfig.enum.Arch.RISCV64":"RISCV64", + "modid.midnightconfig.enum.OS.LINUX":"Linux", + "modid.midnightconfig.enum.OS.WINDOWS":"Windows", + "modid.midnightconfig.enum.OS.MAC":"MacOS", + "modid.midnightconfig.enum.ModPlatform.FORGE":"Forge", + "modid.midnightconfig.enum.ModPlatform.FABRIC":"Fabric", + "modid.midnightconfig.enum.ModPlatform.QUILT":"Quilt", + "modid.midnightconfig.enum.ModPlatform.NEOFORGE":"NeoForge", + "modid.midnightconfig.enum.ModPlatform.VANILLA":"Vanilla", "modid.midnightconfig.myFileOrDirectory.fileChooser": "Select an image or directory", "modid.midnightconfig.myFileOrDirectory.fileFilter": "Supported Images (.png, .jpg, .jpeg)", "modid.midnightconfig.category.numbers": "Numbers", @@ -21,5 +30,6 @@ "modid.midnightconfig.category.sliders": "Sliders", "modid.midnightconfig.category.lists": "Lists", "modid.midnightconfig.category.files": "Files", - "modid.midnightconfig.category.conditions": "Quiz" + "modid.midnightconfig.category.conditions": "Quiz", + "modid.midnightconfig.category.multiConditions": "Multi-Conditions" } \ No newline at end of file From 170363ef359167960d5e56d785a6d8b7f5ee3920 Mon Sep 17 00:00:00 2001 From: Jaffe2718 Date: Thu, 3 Apr 2025 11:15:03 +0800 Subject: [PATCH 04/11] chore: bump version --- gradle.properties | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index beb5c67..59fae1b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ yarn_mappings=1.21.4+build.1 enabled_platforms=fabric,neoforge archives_base_name=midnightlib -mod_version=1.7.0 +mod_version=1.7.1-rc.1 maven_group=eu.midnightdust release_type=release curseforge_id=488090 @@ -18,6 +18,4 @@ fabric_api_version=0.110.5+1.21.4 neoforge_version=21.4.3-beta yarn_mappings_patch_neoforge_version = 1.21+build.4 -quilt_loader_version=0.19.0-beta.18 -quilt_fabric_api_version=7.0.1+0.83.0-1.20 mod_menu_version = 9.0.0 \ No newline at end of file From f4d11832706f2d6558410e6af355e0de5b137b45 Mon Sep 17 00:00:00 2001 From: Jaffe2718 Date: Thu, 3 Apr 2025 11:19:03 +0800 Subject: [PATCH 05/11] cleanup & suppress unused warnings --- .../main/java/eu/midnightdust/lib/config/MidnightConfig.java | 2 -- .../fabric/example/config/MidnightConfigExample.java | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java b/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java index 63b263f..18ea2c2 100755 --- a/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java +++ b/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java @@ -67,8 +67,6 @@ public abstract class MidnightConfig { this.dataType = getUnderlyingType(field); this.entry = field.getAnnotation(Entry.class); this.comment = field.getAnnotation(Comment.class); - // TODO: use multiple annotations -// this.condition = field.getAnnotation(Condition.class); this.conditions = field.getAnnotationsByType(Condition.class); } else { this.fieldName = ""; this.dataType = null; diff --git a/test-fabric/src/main/java/eu/midnightdust/fabric/example/config/MidnightConfigExample.java b/test-fabric/src/main/java/eu/midnightdust/fabric/example/config/MidnightConfigExample.java index c0fedc6..50495cb 100644 --- a/test-fabric/src/main/java/eu/midnightdust/fabric/example/config/MidnightConfigExample.java +++ b/test-fabric/src/main/java/eu/midnightdust/fabric/example/config/MidnightConfigExample.java @@ -10,7 +10,7 @@ import java.util.List; /** Every option in a MidnightConfig class has to be public and static, so we can access it from other classes. * The config class also has to extend MidnightConfig*/ - +@SuppressWarnings("unused") public class MidnightConfigExample extends MidnightConfig { public static final String TEXT = "text"; public static final String NUMBERS = "numbers"; From ac5a035906a63f85ffb612939ae8220e522a2610 Mon Sep 17 00:00:00 2001 From: Jaffe2718 Date: Thu, 3 Apr 2025 13:30:46 +0800 Subject: [PATCH 06/11] optimize: use hash tables to reduce complexity --- .../lib/config/MidnightConfig.java | 81 ++++++++++--------- gradle.properties | 2 +- 2 files changed, 42 insertions(+), 41 deletions(-) diff --git a/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java b/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java index 18ea2c2..10c2668 100755 --- a/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java +++ b/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java @@ -14,7 +14,6 @@ import net.minecraft.registry.Registries; import net.minecraft.screen.ScreenTexts; import net.minecraft.text.Style; import net.minecraft.text.Text; import net.minecraft.util.Formatting; import net.minecraft.util.Identifier; -import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.filechooser.FileNameExtensionFilter; @@ -40,7 +39,9 @@ public abstract class MidnightConfig { private static final Pattern DECIMAL_ONLY = Pattern.compile("-?(\\d+\\.?\\d*|\\d*\\.?\\d+|\\.)"); private static final Pattern HEXADECIMAL_ONLY = Pattern.compile("(-?[#0-9a-fA-F]*)"); - private static final List entries = new ArrayList<>(); +// private static final List entries = new ArrayList<>(); + private static final Hashtable entries = new Hashtable<>(); // modid:fieldName -> EntryInfo + private static final List entryOrder = Lists.newArrayList(); // ordered list of entries private static boolean reloadScreen = false; public static class EntryInfo { @@ -86,7 +87,7 @@ public abstract class MidnightConfig { } public void updateFieldValue() { try { - if (this.field.get(null) != value) entries.forEach(EntryInfo::updateConditions); + if (this.field.get(null) != value) entries.values().forEach(EntryInfo::updateConditions); this.field.set(null, this.value); } catch (IllegalAccessException ignored) {} } @@ -95,16 +96,12 @@ public abstract class MidnightConfig { boolean prevConditionState = this.conditionsMet; if (this.conditions.length > 0) this.conditionsMet = true; // reset conditions for (Condition condition : this.conditions) { - // TODO: redefine entries as a HashMap to optimize complexity - for (EntryInfo info : entries) { - if (((condition.requiredOption().contains(":") ? "" : (this.modid + ":")) + condition.requiredOption()).equals(info.modid + ":" + info.fieldName)) { - this.conditionsMet &= info.tempValue.equals(condition.requiredValue()); -// System.out.println(this.modid + ":" + this.fieldName + "#" + condition.requiredOption() + ": " + condition.requiredValue() + " " + info.tempValue); - } - if (!condition.requiredModId().isEmpty() && !PlatformFunctions.isModLoaded(condition.requiredModId())) { - this.conditionsMet = false; - } - if (!this.conditionsMet) break; + if (!condition.requiredModId().isEmpty() && !PlatformFunctions.isModLoaded(condition.requiredModId())) { + this.conditionsMet = false; + } + String requiredOption = condition.requiredOption().contains(":") ? condition.requiredOption() : (this.modid + ":" + condition.requiredOption()); + if (entries.get(requiredOption) instanceof EntryInfo info) { + this.conditionsMet &= condition.requiredValue().equals(info.tempValue); } if (!this.conditionsMet) break; } @@ -130,19 +127,21 @@ public abstract class MidnightConfig { @SuppressWarnings("unused") // Utility for mod authors public static @Nullable Object getDefaultValue(String modid, String entry) { - for (EntryInfo e : entries) { - if (modid.equals(e.modid) && entry.equals(e.field.getName())) return e.defaultValue; - } return null; + String key = modid + ":" + entry; + return entries.containsKey(key) ? entries.get(key).defaultValue : null; } public static void loadValuesFromJson(String modid) { try { gson.fromJson(Files.newBufferedReader(path), configClass.get(modid)); } catch (Exception e) { write(modid); } - - for (EntryInfo info : entries) if (info.field != null && info.entry != null) { - try { info.value = info.field.get(null); info.tempValue = info.toTemporaryValue(); - info.updateConditions(); - } catch (IllegalAccessException ignored) {} - } + entries.values().forEach(info -> { + if (info.field != null && info.entry != null) { + try { + info.value = info.field.get(null); + info.tempValue = info.toTemporaryValue(); + info.updateConditions(); + } catch (IllegalAccessException ignored) {} + } + }); } public static void init(String modid, Class config) { path = PlatformFunctions.getConfigDirectory().resolve(modid + ".json"); @@ -161,6 +160,7 @@ public abstract class MidnightConfig { @Environment(EnvType.CLIENT) private static void initClient(String modid, Field field, EntryInfo info) { Entry e = info.entry; + String key = modid + ":" + field.getName(); if (e != null) { 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); @@ -185,7 +185,8 @@ public abstract class MidnightConfig { }, func); } } - entries.add(info); + entries.put(key, info); + entryOrder.add(key); } public static Class getUnderlyingType(Field field) { Class rawType = field.getType(); @@ -218,7 +219,7 @@ public abstract class MidnightConfig { info.tempValue = s; t.setEditableColor(inLimits? 0xFFFFFFFF : 0xFFFF7777); info.inLimits = inLimits; - b.active = entries.stream().allMatch(e -> e.inLimits); + b.active = entries.values().stream().allMatch(e -> e.inLimits); if (inLimits) { if (info.dataType == Identifier.class) info.setValue(Identifier.tryParse(s)); @@ -255,17 +256,18 @@ public abstract class MidnightConfig { this.parent = parent; this.modid = modid; this.translationPrefix = modid + ".midnightconfig."; loadValuesFromJson(modid); - - for (EntryInfo e : entries) if (e.modid.equals(modid)) { - String tabId = e.entry != null ? e.entry.category() : e.comment.category(); - String name = translationPrefix + "category." + tabId; - if (!I18n.hasTranslation(name) && tabId.equals("default")) - name = translationPrefix + "title"; - if (!tabs.containsKey(name)) { - Tab tab = new GridScreenTab(Text.translatable(name)); - e.tab = tab; tabs.put(name, tab); - } else e.tab = tabs.get(name); - } + entryOrder.stream().map(entries::get).forEach(info -> { + if (info.modid.equals(modid)) { + String tabId = info.entry != null ? info.entry.category() : info.comment.category(); + String name = translationPrefix + "category." + tabId; + if (!I18n.hasTranslation(name) && tabId.equals("default")) + name = translationPrefix + "title"; + if (!tabs.containsKey(name)) { + Tab tab = new GridScreenTab(Text.translatable(name)); + info.tab = tab; tabs.put(name, tab); + } else info.tab = tabs.get(name); + } + }); tabNavigation = TabNavigationWidget.builder(tabManager, this.width).tabs(tabs.values().toArray(new Tab[0])).build(); tabNavigation.selectTab(0, false); tabNavigation.init(); @@ -290,7 +292,7 @@ public abstract class MidnightConfig { updateList(); list.setScrollY(0); } scrollProgress = list.getScrollY(); - for (EntryInfo info : entries) info.updateFieldValue(); + for (EntryInfo info : entries.values()) info.updateFieldValue(); updateButtons(); if (reloadScreen) { updateList(); reloadScreen = false; } } @@ -314,7 +316,7 @@ public abstract class MidnightConfig { Objects.requireNonNull(client).setScreen(parent); } private void cleanup() { - entries.forEach(info -> { + entries.values().forEach(info -> { info.error = null; info.value = null; info.tempValue = null; info.actionButton = null; info.listIndex = 0; info.tab = null; info.inLimits = true; }); } @@ -326,7 +328,7 @@ public abstract class MidnightConfig { this.addDrawableChild(ButtonWidget.builder(ScreenTexts.CANCEL, button -> this.close()).dimensions(this.width / 2 - 154, this.height - 26, 150, 20).build()); done = this.addDrawableChild(ButtonWidget.builder(ScreenTexts.DONE, (button) -> { - for (EntryInfo info : entries) if (info.modid.equals(modid)) info.updateFieldValue(); + for (EntryInfo info : entries.values()) if (info.modid.equals(modid)) info.updateFieldValue(); write(modid); cleanup(); Objects.requireNonNull(client).setScreen(parent); }).dimensions(this.width / 2 + 4, this.height - 26, 150, 20).build()); @@ -339,8 +341,7 @@ public abstract class MidnightConfig { this.list.clear(); fillList(); } public void fillList() { - for (EntryInfo info : entries) { -// if (!info.conditionsMet && info.condition != null && !info.condition.visibleButLocked()) continue; + for (EntryInfo info : entryOrder.stream().map(entries::get).toList()) { if (!info.conditionsMet) { boolean visibleButLocked = false; for (Condition condition : info.conditions) { diff --git a/gradle.properties b/gradle.properties index 59fae1b..8435a28 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ yarn_mappings=1.21.4+build.1 enabled_platforms=fabric,neoforge archives_base_name=midnightlib -mod_version=1.7.1-rc.1 +mod_version=1.7.1-rc.2 maven_group=eu.midnightdust release_type=release curseforge_id=488090 From ee705b2cbdfc4a96db858c8f8abc19b3c276f6af Mon Sep 17 00:00:00 2001 From: Martin Prokoph Date: Sat, 5 Apr 2025 11:47:04 +0200 Subject: [PATCH 07/11] feat: replace Hashtable with LinkedHashMap - Keeps the entry order in tact without the need to store it separately --- .../midnightdust/lib/config/MidnightConfig.java | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java b/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java index 10c2668..ddbd496 100755 --- a/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java +++ b/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java @@ -39,15 +39,12 @@ public abstract class MidnightConfig { private static final Pattern DECIMAL_ONLY = Pattern.compile("-?(\\d+\\.?\\d*|\\d*\\.?\\d+|\\.)"); private static final Pattern HEXADECIMAL_ONLY = Pattern.compile("(-?[#0-9a-fA-F]*)"); -// private static final List entries = new ArrayList<>(); - private static final Hashtable entries = new Hashtable<>(); // modid:fieldName -> EntryInfo - private static final List entryOrder = Lists.newArrayList(); // ordered list of entries + private static final LinkedHashMap entries = new LinkedHashMap<>(); // modid:fieldName -> EntryInfo private static boolean reloadScreen = false; public static class EntryInfo { public Entry entry; public Comment comment; -// @ApiStatus.Obsolete public Condition condition; public Condition[] conditions; public final Field field; public final Class dataType; @@ -96,13 +93,11 @@ public abstract class MidnightConfig { boolean prevConditionState = this.conditionsMet; if (this.conditions.length > 0) this.conditionsMet = true; // reset conditions for (Condition condition : this.conditions) { - if (!condition.requiredModId().isEmpty() && !PlatformFunctions.isModLoaded(condition.requiredModId())) { + if (!condition.requiredModId().isEmpty() && !PlatformFunctions.isModLoaded(condition.requiredModId())) this.conditionsMet = false; - } String requiredOption = condition.requiredOption().contains(":") ? condition.requiredOption() : (this.modid + ":" + condition.requiredOption()); - if (entries.get(requiredOption) instanceof EntryInfo info) { + if (entries.get(requiredOption) instanceof EntryInfo info) this.conditionsMet &= condition.requiredValue().equals(info.tempValue); - } if (!this.conditionsMet) break; } if (prevConditionState != this.conditionsMet) reloadScreen = true; @@ -186,7 +181,6 @@ public abstract class MidnightConfig { } } entries.put(key, info); - entryOrder.add(key); } public static Class getUnderlyingType(Field field) { Class rawType = field.getType(); @@ -256,7 +250,7 @@ public abstract class MidnightConfig { this.parent = parent; this.modid = modid; this.translationPrefix = modid + ".midnightconfig."; loadValuesFromJson(modid); - entryOrder.stream().map(entries::get).forEach(info -> { + entries.forEach((id, info) -> { if (info.modid.equals(modid)) { String tabId = info.entry != null ? info.entry.category() : info.comment.category(); String name = translationPrefix + "category." + tabId; @@ -341,7 +335,7 @@ public abstract class MidnightConfig { this.list.clear(); fillList(); } public void fillList() { - for (EntryInfo info : entryOrder.stream().map(entries::get).toList()) { + for (EntryInfo info : entries.sequencedValues()) { if (!info.conditionsMet) { boolean visibleButLocked = false; for (Condition condition : info.conditions) { From 64be14f1362382ec33db4c972a344345294d794c Mon Sep 17 00:00:00 2001 From: Martin Prokoph Date: Sat, 5 Apr 2025 12:02:58 +0200 Subject: [PATCH 08/11] clean: improve entry access further --- .../main/java/eu/midnightdust/lib/config/MidnightConfig.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java b/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java index ddbd496..4487db3 100755 --- a/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java +++ b/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java @@ -174,7 +174,6 @@ public abstract class MidnightConfig { }; info.function = new AbstractMap.SimpleEntry>(button -> { int index = values.indexOf(info.value) + 1; - //info.value = values.get(index >= values.size() ? 0 : index); button.setMessage(func.apply(info.value)); info.setValue(values.get(index >= values.size() ? 0 : index)); button.setMessage(func.apply(info.value)); }, func); @@ -250,7 +249,7 @@ public abstract class MidnightConfig { this.parent = parent; this.modid = modid; this.translationPrefix = modid + ".midnightconfig."; loadValuesFromJson(modid); - entries.forEach((id, info) -> { + entries.values().forEach(info -> { if (info.modid.equals(modid)) { String tabId = info.entry != null ? info.entry.category() : info.comment.category(); String name = translationPrefix + "category." + tabId; @@ -335,7 +334,7 @@ public abstract class MidnightConfig { this.list.clear(); fillList(); } public void fillList() { - for (EntryInfo info : entries.sequencedValues()) { + for (EntryInfo info : entries.values()) { if (!info.conditionsMet) { boolean visibleButLocked = false; for (Condition condition : info.conditions) { From 386a95aca82d2a81c78887a398e3973d70d00410 Mon Sep 17 00:00:00 2001 From: Martin Prokoph Date: Sat, 5 Apr 2025 12:10:11 +0200 Subject: [PATCH 09/11] feat: optimize AutoCommand's file-size --- .../midnightdust/lib/config/AutoCommand.java | 40 +++++++++---------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/common/src/main/java/eu/midnightdust/lib/config/AutoCommand.java b/common/src/main/java/eu/midnightdust/lib/config/AutoCommand.java index 95571c8..e41f920 100644 --- a/common/src/main/java/eu/midnightdust/lib/config/AutoCommand.java +++ b/common/src/main/java/eu/midnightdust/lib/config/AutoCommand.java @@ -18,44 +18,41 @@ public class AutoCommand { final Field field; final Class type; final String modid; - final boolean isList, isNumber; + final boolean isList; public AutoCommand(Field field, String modid) { this.field = field; this.modid = modid; this.type = MidnightConfig.getUnderlyingType(field); this.isList = field.getType() == List.class; - this.isNumber = type == int.class || type == double.class || type == float.class; var command = CommandManager.literal(field.getName()).executes(this::getValue); if (type.isEnum()) { for (Object enumValue : field.getType().getEnumConstants()) - command = command.then(CommandManager.literal(enumValue.toString()).executes(ctx -> this.setValue(ctx.getSource(), enumValue, ""))); + command = command.then(CommandManager.literal(enumValue.toString()) + .executes(ctx -> this.setValue(ctx.getSource(), enumValue, ""))); } else if (isList) { - for (String action : List.of("add", "remove")) - command = command.then(CommandManager.literal(action).then( - CommandManager.argument(VALUE, getArgType()).executes(ctx -> setValueFromArg(ctx, action)))); + for (String action : new String[]{"add", "remove"}) + command = command.then(CommandManager.literal(action) + .then(CommandManager.argument(VALUE, getArgType()).executes(ctx -> setValueFromArg(ctx, action)))); } else command = command.then(CommandManager.argument(VALUE, getArgType()).executes(ctx -> setValueFromArg(ctx, ""))); PlatformFunctions.registerCommand(CommandManager.literal("midnightconfig").requires(source -> source.hasPermissionLevel(2)).then(CommandManager.literal(modid).then(command))); } public ArgumentType getArgType() { - if (isNumber) { - Entry entry = field.getAnnotation(Entry.class); - if (type == int.class) return IntegerArgumentType.integer((int) entry.min(), (int) entry.max()); - else if (type == double.class) return DoubleArgumentType.doubleArg(entry.min(), entry.max()); - else if (type == float.class) return FloatArgumentType.floatArg((float) entry.min(), (float) entry.max()); - } + Entry entry = field.getAnnotation(Entry.class); + if (type == int.class) return IntegerArgumentType.integer((int) entry.min(), (int) entry.max()); + else if (type == double.class) return DoubleArgumentType.doubleArg(entry.min(), entry.max()); + else if (type == float.class) return FloatArgumentType.floatArg((float) entry.min(), (float) entry.max()); else if (type == boolean.class) return BoolArgumentType.bool(); return StringArgumentType.string(); } + public int setValueFromArg(CommandContext context, String action) { - if (isNumber) { - if (type == int.class) return setValue(context.getSource(), IntegerArgumentType.getInteger(context, VALUE), action); - else if (type == double.class) return setValue(context.getSource(), DoubleArgumentType.getDouble(context, VALUE), action); - else if (type == float.class) return setValue(context.getSource(), FloatArgumentType.getFloat(context, VALUE), action); - } + if (type == int.class) return setValue(context.getSource(), IntegerArgumentType.getInteger(context, VALUE), action); + else if (type == double.class) return setValue(context.getSource(), DoubleArgumentType.getDouble(context, VALUE), action); + else if (type == float.class) return setValue(context.getSource(), FloatArgumentType.getFloat(context, VALUE), action); else if (type == boolean.class) return setValue(context.getSource(), BoolArgumentType.getBool(context, VALUE), action); return setValue(context.getSource(), StringArgumentType.getString(context, VALUE), action); } @@ -72,17 +69,16 @@ public class AutoCommand { MidnightConfig.write(modid); } catch (Exception e) { - if (!isList) source.sendError(Text.literal("Could not set "+field.getName()+" to value "+value+": " + e)); - else source.sendError(Text.literal((add ? "Could not add "+value+" to " : "Could not remove "+value+" from ")+field.getName() +": " + e)); + source.sendError(Text.literal(isList ? "Could not %s %s %s %s: %s".formatted(add ? "add" : "remove", value, add ? "to" : "from", field.getName(), e) : "Could not set %s to value %s: %s".formatted(field.getName(), value, e))); return 0; } - if (!isList) source.sendFeedback(() -> Text.literal("Successfully set " + field.getName()+" to "+value), true); - else source.sendFeedback(() -> Text.literal((add ? "Successfully added " +value+" to " : "Successfully removed " +value+" from ") +field.getName()), true); + source.sendFeedback(() -> Text.literal(isList ? "Successfully %s %s %s %s".formatted(add ? "added" : "removed", value, add ? "to" : "from", field.getName()) : + "Successfully set %s to %s".formatted(field.getName(), value)), true); return 1; } private int getValue(CommandContext context) { context.getSource().sendFeedback(() -> { - try { return Text.literal("The value of "+field.getName()+" is "+field.get(null)); + try { return Text.literal("The value of %s is %s".formatted(field.getName(), field.get(null))); } catch (IllegalAccessException e) {throw new RuntimeException(e);} }, true); return 0; From 9b29b208ebbcaf57999068890e45b2b246b12538 Mon Sep 17 00:00:00 2001 From: Martin Prokoph Date: Sat, 5 Apr 2025 12:38:38 +0200 Subject: [PATCH 10/11] docs: improve multi-conditions example --- .../example/config/MidnightConfigExample.java | 39 +++++-------------- 1 file changed, 9 insertions(+), 30 deletions(-) diff --git a/test-fabric/src/main/java/eu/midnightdust/fabric/example/config/MidnightConfigExample.java b/test-fabric/src/main/java/eu/midnightdust/fabric/example/config/MidnightConfigExample.java index 50495cb..ec599de 100644 --- a/test-fabric/src/main/java/eu/midnightdust/fabric/example/config/MidnightConfigExample.java +++ b/test-fabric/src/main/java/eu/midnightdust/fabric/example/config/MidnightConfigExample.java @@ -18,7 +18,6 @@ public class MidnightConfigExample extends MidnightConfig { public static final String LISTS = "lists"; public static final String FILES = "files"; public static final String CONDITIONS = "conditions"; - public static final String MULTI_CONDITIONS = "multiConditions"; @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! @@ -77,18 +76,27 @@ public class MidnightConfigExample extends MidnightConfig { @Condition(requiredOption = "modid:turnMeOn", visibleButLocked = true) @Entry(category = CONDITIONS, name="Turn me off (locked if modid:turnMeOn is false)!") public static Boolean turnMeOff = true; + @Condition(requiredOption = "turnMeOn") // You can also use multiple conditions for the same entry @Condition(requiredOption = "modid:turnMeOff", requiredValue = "false") @Entry(category = CONDITIONS, name="Which is the best modloader?") public static String bestModloader = ""; + @Condition(requiredOption = "turnMeOn") + @Condition(requiredOption = "turnMeOff", requiredValue = "false") @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 = "turnMeOn") + @Condition(requiredOption = "turnMeOff", requiredValue = "false") @Condition(requiredOption = "bestModloader", requiredValue = "NeoForge") @Comment(category = CONDITIONS, name="⛏ Not quite, but it's alright!", centered = true) public static Comment answerNeoforge; + @Condition(requiredOption = "turnMeOn") + @Condition(requiredOption = "turnMeOff", requiredValue = "false") @Condition(requiredOption = "bestModloader", requiredValue = "Fabric") @Comment(category = CONDITIONS, name="⭐ Correct! Fabric (and Quilt) are the best!", centered = true) public static Comment answerFabric; + @Condition(requiredOption = "turnMeOn") + @Condition(requiredOption = "turnMeOff", requiredValue = "false") @Condition(requiredOption = "bestModloader", requiredValue = "Quilt") @Comment(category = CONDITIONS, name="⭐ Correct! Quilt (and Fabric) are the best!", centered = true) public static Comment answerQuilt; @@ -98,34 +106,5 @@ public class MidnightConfigExample extends MidnightConfig { @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; - - // Multi-Conditions are also supported! - public enum Arch {X86, X86_64, AARCH64, RISCV64} - public enum OS {WINDOWS, MAC, LINUX} - @Entry(category = MULTI_CONDITIONS, name = "Arch") public static Arch myArch = Arch.X86; - @Entry(category = MULTI_CONDITIONS, name = "OS") public static OS myOS = OS.WINDOWS; - @Entry(category = MULTI_CONDITIONS, name = "Mod Platform") public static ModPlatform myPlatform = ModPlatform.FABRIC; - - @Condition(requiredOption = "modid:myArch", requiredValue = "X86_64") - @Condition(requiredOption = "modid:myOS", requiredValue = "WINDOWS") - @Condition(requiredOption = "modid:myPlatform", requiredValue = "FABRIC") - @Comment(category = MULTI_CONDITIONS, name = "MidnightLib can be used on Windows x86_64 with Fabric!", centered = true) public static Comment x86_64_windows_fabric; - - @Condition(requiredOption = "modid:myArch", requiredValue = "X86_64") - @Condition(requiredOption = "modid:myOS", requiredValue = "LINUX") - @Condition(requiredOption = "modid:myPlatform", requiredValue = "FABRIC") - @Comment(category = MULTI_CONDITIONS, name = "MidnightLib can be used on Linux x86_64 with Fabric!", centered = true) public static Comment x86_64_linux_fabric; - - @Condition(requiredOption = "modid:myArch", requiredValue = "X86_64") - @Condition(requiredOption = "modid:myOS", requiredValue = "WINDOWS") - @Condition(requiredOption = "modid:myPlatform", requiredValue = "NEOFORGE") - @Comment(category = MULTI_CONDITIONS, name = "MidnightLib can be used on Windows x86_64 with NeoForge!", centered = true) public static Comment x86_64_windows_neoforge; - - @Condition(requiredOption = "modid:myArch", requiredValue = "X86_64") - @Condition(requiredOption = "modid:myOS", requiredValue = "LINUX") - @Condition(requiredOption = "modid:myPlatform", requiredValue = "NEOFORGE") - @Comment(category = MULTI_CONDITIONS, name = "MidnightLib can be used on Linux x86_64 with NeoForge!", centered = true) public static Comment x86_64_linux_neoforge; - - public static int imposter = 16777215; // - Entries without an @Entry or @Comment annotation are ignored } \ No newline at end of file From a3f4dfc9db9ac50207455604e27f0e63e817bec9 Mon Sep 17 00:00:00 2001 From: Martin Prokoph Date: Sat, 5 Apr 2025 12:43:07 +0200 Subject: [PATCH 11/11] chore: bump version --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 8435a28..6903179 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,12 +1,12 @@ org.gradle.jvmargs=-Xmx4096M minecraft_version=1.21.4 -supported_versions= +supported_versions=1.21.5 yarn_mappings=1.21.4+build.1 enabled_platforms=fabric,neoforge archives_base_name=midnightlib -mod_version=1.7.1-rc.2 +mod_version=1.7.1 maven_group=eu.midnightdust release_type=release curseforge_id=488090