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; 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..4487db3 100755 --- a/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java +++ b/common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java @@ -19,7 +19,7 @@ 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.*; @@ -39,13 +39,13 @@ 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 LinkedHashMap entries = new LinkedHashMap<>(); // modid:fieldName -> EntryInfo private static boolean reloadScreen = false; public static class EntryInfo { public Entry entry; public Comment comment; - public Condition condition; + public Condition[] conditions; public final Field field; public final Class dataType; public final String modid, fieldName; @@ -61,8 +61,11 @@ 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); + this.conditions = field.getAnnotationsByType(Condition.class); } else { this.fieldName = ""; this.dataType = null; } @@ -80,19 +83,24 @@ 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.values().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) { + 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; } + if (prevConditionState != this.conditionsMet) reloadScreen = true; } public void writeList(int index, T value) { var list = (List) this.value; @@ -114,19 +122,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(info.tempValue); - } 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"); @@ -145,6 +155,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); @@ -163,11 +174,12 @@ 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); } } - entries.add(info); + entries.put(key, info); } public static Class getUnderlyingType(Field field) { Class rawType = field.getType(); @@ -200,7 +212,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)); @@ -237,17 +249,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); - } + 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; + 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(); @@ -272,7 +285,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; } } @@ -296,7 +309,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; }); } @@ -308,7 +321,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()); @@ -321,8 +334,14 @@ 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 : entries.values()) { + 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 -> { @@ -565,6 +584,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 ""; @@ -572,4 +592,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 diff --git a/gradle.properties b/gradle.properties index beb5c67..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.0 +mod_version=1.7.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 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..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 @@ -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"; @@ -27,9 +27,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,20 +74,29 @@ 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 = "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; 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