From 5d0ad68c64a0ce008c9169967ba0cb9c10c960b8 Mon Sep 17 00:00:00 2001 From: hypherionmc Date: Tue, 14 Jan 2025 15:53:58 +0200 Subject: [PATCH] [DEV] New Cloth Config GUIs, new nojang apis, and bug fixes --- .jenkins/Jenkinsfile.snapshot | 2 +- Common/build.gradle | 2 + .../craterlib/api/commands/CraterCommand.java | 12 +- .../events/compat/LuckPermsCompatEvents.java | 3 + .../gui/config/ClothConfigScreenBuilder.java | 409 ++++++++++++++++++ .../client/gui/config/CraterConfigScreen.java | 1 + .../config/widgets/AbstractConfigWidget.java | 1 + .../client/gui/config/widgets/BaseWidget.java | 1 + .../widgets/ClothConfigButtonEntry.java | 128 ++++++ .../config/widgets/InternalConfigButton.java | 1 + .../client/gui/config/widgets/Option.java | 1 + .../gui/config/widgets/SubConfigWidget.java | 1 + .../gui/config/widgets/TextConfigOption.java | 1 + .../gui/config/widgets/ToggleButton.java | 1 + .../gui/config/widgets/WrappedEditBox.java | 1 + .../core/config/annotations/ClothScreen.java | 12 + .../nojang/client/gui/BridgedScreen.java | 13 +- .../world/entity/player/BridgedPlayer.java | 29 ++ .../assets/craterlib/lang/en_us.json | 6 +- Fabric/build.gradle | 7 +- .../CraterLibModMenuIntegration.java | 14 +- Forge/build.gradle | 10 + .../mixin/ConfigScreenHandlerMixin.java | 15 +- NeoForge/build.gradle | 14 +- .../client/NeoForgeClientHelper.java | 14 +- changelog.md | 13 +- gradle.properties | 5 +- 27 files changed, 682 insertions(+), 35 deletions(-) create mode 100644 Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/ClothConfigScreenBuilder.java create mode 100644 Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/ClothConfigButtonEntry.java create mode 100644 Common/src/main/java/com/hypherionmc/craterlib/core/config/annotations/ClothScreen.java diff --git a/.jenkins/Jenkinsfile.snapshot b/.jenkins/Jenkinsfile.snapshot index e9b9008..ba41d19 100644 --- a/.jenkins/Jenkinsfile.snapshot +++ b/.jenkins/Jenkinsfile.snapshot @@ -3,7 +3,7 @@ def projectIcon = "https://cdn.modrinth.com/data/Nn8Wasaq/a172c634683a11a2e9ae59 def JDK = "21"; def majorMc = "1.21.2"; def modLoaders = "neoforge|fabric|quilt|paper"; -def supportedMc = "1.21.3"; +def supportedMc = "1.21.3|1.21.4"; def reltype = "port"; pipeline { diff --git a/Common/build.gradle b/Common/build.gradle index 81fbb3f..87a50e1 100644 --- a/Common/build.gradle +++ b/Common/build.gradle @@ -3,6 +3,8 @@ archivesBaseName = "${mod_name.replace(" ", "")}-Common-${minecraft_version}" dependencies { stupidRemapArch("dev.ftb.mods:ftb-essentials:${ftb_essentials}") stupidRemapArch("dev.ftb.mods:ftb-ranks:${ftb_ranks}") + + stupidRemapArch("me.shedaniel.cloth:cloth-config:${cloth_config}") } shadowJar { diff --git a/Common/src/main/java/com/hypherionmc/craterlib/api/commands/CraterCommand.java b/Common/src/main/java/com/hypherionmc/craterlib/api/commands/CraterCommand.java index 35f29f0..9e3d459 100644 --- a/Common/src/main/java/com/hypherionmc/craterlib/api/commands/CraterCommand.java +++ b/Common/src/main/java/com/hypherionmc/craterlib/api/commands/CraterCommand.java @@ -1,5 +1,6 @@ package com.hypherionmc.craterlib.api.commands; +import com.hypherionmc.craterlib.CraterConstants; import com.hypherionmc.craterlib.compat.LuckPermsCompat; import com.hypherionmc.craterlib.core.platform.ModloaderEnvironment; import com.hypherionmc.craterlib.nojang.authlib.BridgedGameProfile; @@ -137,10 +138,15 @@ public class CraterCommand { } private boolean checkPermission(CommandSourceStack stack) { - if (!ModloaderEnvironment.INSTANCE.isModLoaded("luckperms") || !stack.isPlayer() || luckPermNode.isEmpty()) - return stack.hasPermission(this.permLevel); + try { + if (!ModloaderEnvironment.INSTANCE.isModLoaded("luckperms") || !stack.isPlayer() || luckPermNode.isEmpty()) + return stack.hasPermission(this.permLevel); - return LuckPermsCompat.INSTANCE.hasPermission(stack.getPlayer(), this.luckPermNode) || stack.hasPermission(this.permLevel); + return LuckPermsCompat.INSTANCE.hasPermission(stack.getPlayer(), this.luckPermNode) || stack.hasPermission(this.permLevel); + } catch (Exception e) { + CraterConstants.LOG.error("Failed to check luckperms permissions", e); + return stack.hasPermission(this.permLevel); + } } @FunctionalInterface diff --git a/Common/src/main/java/com/hypherionmc/craterlib/api/events/compat/LuckPermsCompatEvents.java b/Common/src/main/java/com/hypherionmc/craterlib/api/events/compat/LuckPermsCompatEvents.java index 69e75ad..0057e4d 100644 --- a/Common/src/main/java/com/hypherionmc/craterlib/api/events/compat/LuckPermsCompatEvents.java +++ b/Common/src/main/java/com/hypherionmc/craterlib/api/events/compat/LuckPermsCompatEvents.java @@ -2,6 +2,7 @@ package com.hypherionmc.craterlib.api.events.compat; import com.hypherionmc.craterlib.core.event.CraterEvent; import com.hypherionmc.craterlib.nojang.authlib.BridgedGameProfile; +import lombok.Getter; import lombok.RequiredArgsConstructor; import java.util.UUID; @@ -9,6 +10,7 @@ import java.util.UUID; public class LuckPermsCompatEvents { @RequiredArgsConstructor(staticName = "of") + @Getter public static class GroupAddedEvent extends CraterEvent { private final String identifier; private final UUID uuid; @@ -20,6 +22,7 @@ public class LuckPermsCompatEvents { } @RequiredArgsConstructor(staticName = "of") + @Getter public static class GroupRemovedEvent extends CraterEvent { private final String identifier; private final UUID uuid; diff --git a/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/ClothConfigScreenBuilder.java b/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/ClothConfigScreenBuilder.java new file mode 100644 index 0000000..192c6ac --- /dev/null +++ b/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/ClothConfigScreenBuilder.java @@ -0,0 +1,409 @@ +package com.hypherionmc.craterlib.client.gui.config; + +import com.hypherionmc.craterlib.CraterConstants; +import com.hypherionmc.craterlib.client.gui.config.widgets.ClothConfigButtonEntry; +import com.hypherionmc.craterlib.core.config.AbstractConfig; +import com.hypherionmc.craterlib.core.config.annotations.HideFromScreen; +import com.hypherionmc.craterlib.core.config.annotations.SubConfig; +import com.hypherionmc.craterlib.core.config.annotations.Tooltip; +import me.hypherionmc.moonconfig.core.conversion.SpecComment; +import me.shedaniel.clothconfig2.api.ConfigBuilder; +import me.shedaniel.clothconfig2.api.ConfigCategory; +import me.shedaniel.clothconfig2.api.ConfigEntryBuilder; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.toasts.SystemToast; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; + +/** + * @author HypherionSA + * A Helper Class to convert {@link AbstractConfig}s into Cloth Config Screens + */ +@SuppressWarnings({"rawtypes", "unchecked"}) +@ApiStatus.Internal +public class ClothConfigScreenBuilder { + + /** + * Build a new Cloth Config screen, from an {@link AbstractConfig} + * + * @param config The {@link AbstractConfig} the config screen is for + * @param parent The parent {@link Screen} this screen will return to when closed + * @return A fully usable Cloth Config screen + */ + public static Screen buildConfigScreen(AbstractConfig config, @Nullable Screen parent) { + ConfigBuilder builder = ConfigBuilder.create() + .setParentScreen(parent) + .setTitle(getTranslationKey(config, config, null)); + + readConfigFields(config, config, builder); + + builder.setSavingRunnable(() -> safeSaveConfig(config)); + return builder.build(); + } + + /** + * Build a new sub-screen for config entries marked with {@link SubConfig} + * + * @param config The {@link AbstractConfig} the config screen is for + * @param clazz The object or class that the screen is being built for + * @param parent The parent {@link Screen} this screen will return to when closed + * @return A fully usable Cloth Config screen + */ + private static Screen buildSubScreen(AbstractConfig config, Object clazz, @Nullable Screen parent) { + ConfigBuilder builder = ConfigBuilder.create() + .setParentScreen(parent) + .setTitle(getTranslationKey(config, clazz, null)); + + readConfigFields(config, clazz, builder); + + builder.setSavingRunnable(() -> safeSaveConfig(config)); + return builder.build(); + } + + /** + * Build a new screen, that allows editing lists of complex objects, like a list of Classes + * + * @param config The {@link AbstractConfig} the config screen is for + * @param list The list of objects this screen will be responsible for + * @param field The field this list belongs to in the main config + * @param invoker The object used to invoke the field, when setting the new value + * @param parent The parent {@link Screen} this screen will return to when closed + * @param edited Was this list edited + * @return A fully usable Cloth Config screen + */ + private static Screen buildListScreen(AbstractConfig config, List list, Field field, Object invoker, @Nullable Screen parent, boolean edited) { + ConfigBuilder builder = ConfigBuilder.create() + .setParentScreen(parent) + .setTitle(getTranslationKey(config, invoker, field.getName())); + + ConfigCategory category = builder.getOrCreateCategory(Component.literal("Entries")); + + // Handle Existing items in the list + for (int i = 0; i < list.size(); i++) { + Object item = list.get(i); + + int finalI = i; + // Add a button to open the edit screen, as well as a delete button + category.addEntry( + new ClothConfigButtonEntry( + Component.translatable("cl.buttons.entry", (i + 1)), + Component.translatable("cl.buttons.edit"), + button -> Minecraft.getInstance().setScreen( + buildSubScreen(config, item, builder.build()) + ), + button -> { + list.remove(finalI); + saveFieldValue(list, field, invoker); + Minecraft.getInstance().setScreen(buildListScreen(config, list, field, invoker, parent, true)); + }, + edited + ) + ); + } + + // Add a button to add new list entries + Type listType = field.getGenericType(); + if (listType instanceof ParameterizedType paramType) { + Class elementType = (Class) paramType.getActualTypeArguments()[0]; + + category.addEntry( + new ClothConfigButtonEntry( + Component.literal(""), + Component.translatable("cl.buttons.add_entry"), + button -> { + try { + Object newItem = elementType.getDeclaredConstructor().newInstance(); + list.add(newItem); + saveFieldValue(list, field, invoker); + Minecraft.getInstance().setScreen(buildListScreen(config, list, field, invoker, parent, true)); + } catch (Exception e) { + CraterConstants.LOG.error("Failed to create new list entry", e); + } + } + ) + ); + } + + builder.setSavingRunnable(() -> safeSaveConfig(config)); + return builder.build(); + } + + /** + * A helper method to convert an {@link AbstractConfig} into the corresponding cloth config gui fields + * + * @param baseConfig The {@link AbstractConfig} to convert + * @param config The base class that is being processed + * @param builder The {@link ClothConfigScreenBuilder} we are currently working with + */ + private static void readConfigFields(AbstractConfig baseConfig, Object config, ConfigBuilder builder) { + ConfigEntryBuilder entryBuilder = builder.entryBuilder(); + ConfigCategory configCategory = builder.getOrCreateCategory(Component.literal("General")); + + for (Field field : config.getClass().getDeclaredFields()) { + try { + field.setAccessible(true); + + // We ignore static, transient fields or fields marked with @HideFromScreen + final int fieldModifiers = field.getModifiers(); + if (Modifier.isStatic(fieldModifiers) || Modifier.isTransient(fieldModifiers) || field.isAnnotationPresent(HideFromScreen.class)) + continue; + + Object val = field.get(config); + + // Field is marked as sub-config, so we add a button field for it + if (field.isAnnotationPresent(SubConfig.class)) { + if (val != null) { + configCategory.addEntry( + new ClothConfigButtonEntry( + Component.translatable("cl.config." + baseConfig.getClass().getSimpleName().toLowerCase() + "." + field.getName().toLowerCase()), + Component.translatable("cl.buttons.edit"), + button -> Minecraft.getInstance().setScreen( + buildSubScreen(baseConfig, val, builder.build()) + ) + ) + ); + continue; + } + } + + // Boolean Values + if (val instanceof Boolean bool) { + configCategory.addEntry(entryBuilder.startBooleanToggle(getTranslationKey(baseConfig, config, field.getName()), bool) + .setTooltip(getToolTip(field)) + .setSaveConsumer(newValue -> saveFieldValue(newValue, field, config)) + .setDefaultValue(bool).build()); + } + + // Enum Values + if (val instanceof Enum enumValue) { + Class enumClass = (Class)enumValue.getDeclaringClass(); + configCategory.addEntry(entryBuilder.startEnumSelector( + getTranslationKey(baseConfig, config, field.getName()), + enumClass, + enumValue) + .setTooltip(getToolTip(field)) + .setSaveConsumer(newValue -> saveFieldValue(newValue, field, config)) + .setDefaultValue(enumValue) + .build()); + } + + // Int Values + if (val instanceof Integer intt) { + configCategory.addEntry(entryBuilder.startIntField(getTranslationKey(baseConfig, config, field.getName()), intt) + .setTooltip(getToolTip(field)) + .setSaveConsumer(newValue -> saveFieldValue(newValue, field, config)) + .setDefaultValue(intt).build()); + } + + // Long Values + if (val instanceof Long longt) { + configCategory.addEntry(entryBuilder.startLongField(getTranslationKey(baseConfig, config, field.getName()), longt) + .setTooltip(getToolTip(field)) + .setSaveConsumer(newValue -> saveFieldValue(newValue, field, config)) + .setDefaultValue(longt).build()); + } + + // Float Values + if (val instanceof Float floatt) { + configCategory.addEntry(entryBuilder.startFloatField(getTranslationKey(baseConfig, config, field.getName()), floatt) + .setTooltip(getToolTip(field)) + .setSaveConsumer(newValue -> saveFieldValue(newValue, field, config)) + .setDefaultValue(floatt).build()); + } + + // Double Values + if (val instanceof Double doublet) { + configCategory.addEntry(entryBuilder.startDoubleField(getTranslationKey(baseConfig, config, field.getName()), doublet) + .setTooltip(getToolTip(field)) + .setSaveConsumer(newValue -> saveFieldValue(newValue, field, config)) + .setDefaultValue(doublet).build()); + } + + // String Values + if (val instanceof String string) { + configCategory.addEntry(entryBuilder.startStrField(getTranslationKey(baseConfig, config, field.getName()), string) + .setTooltip(getToolTip(field)) + .setSaveConsumer(newValue -> saveFieldValue(newValue, field, config)) + .setDefaultValue(string).build()); + } + + // Lists + if (val instanceof List list) { + Type listType = field.getGenericType(); + if (listType instanceof ParameterizedType paramType) { + Type elementType = paramType.getActualTypeArguments()[0]; + + // String List + if (elementType.equals(String.class)) { + configCategory.addEntry(entryBuilder.startStrList(getTranslationKey(baseConfig, config, field.getName()), (List) list) + .setTooltip(getToolTip(field)) + .setSaveConsumer(newValue -> saveFieldValue(new ArrayList<>(newValue), field, config)) + .setDefaultValue((List) list).build()); + + // Int List + } else if (elementType.equals(Integer.class)) { + configCategory.addEntry(entryBuilder.startIntList(getTranslationKey(baseConfig, config, field.getName()), (List) list) + .setTooltip(getToolTip(field)) + .setSaveConsumer(newValue -> saveFieldValue(new ArrayList<>(newValue), field, config)) + .setDefaultValue((List) list).build()); + + // Long List + } else if (elementType.equals(Long.class)) { + configCategory.addEntry(entryBuilder.startLongList(getTranslationKey(baseConfig, config, field.getName()), (List) list) + .setTooltip(getToolTip(field)) + .setSaveConsumer(newValue -> saveFieldValue(new ArrayList<>(newValue), field, config)) + .setDefaultValue((List) list).build()); + + // Float List + } else if (elementType.equals(Float.class)) { + configCategory.addEntry(entryBuilder.startFloatList(getTranslationKey(baseConfig, config, field.getName()), (List) list) + .setTooltip(getToolTip(field)) + .setSaveConsumer(newValue -> saveFieldValue(new ArrayList<>(newValue), field, config)) + .setDefaultValue((List) list).build()); + + // Double List + } else if (elementType.equals(Double.class)) { + configCategory.addEntry(entryBuilder.startDoubleList(getTranslationKey(baseConfig, config, field.getName()), (List) list) + .setTooltip(getToolTip(field)) + .setSaveConsumer(newValue -> saveFieldValue(new ArrayList<>(newValue), field, config)) + .setDefaultValue((List) list).build()); + } else { + // List of complex objects + configCategory.addEntry( + new ClothConfigButtonEntry( + getTranslationKey(baseConfig, config, field.getName()), + Component.translatable("cl.buttons.edit"), + button -> Minecraft.getInstance().setScreen( + buildListScreen(baseConfig, list, field, config, builder.build(), false) + ) + ) + ); + } + } + } + + } catch (Exception e) { + CraterConstants.LOG.error("Failed to process config file {}", baseConfig.getConfigName(), e); + } + } + } + + /** + * Helper method to supply tooltips to config fields. + * If the field has an {@link SpecComment}, we use that, otherwise we use the {@link Tooltip} annotation + * or generate one from the field name + * + * @param field The field that is being processed + * @return A {@link Component} that can be used for the tooltip + */ + private static Component getToolTip(Field field) { + Component component = Component.empty(); + + if (field.isAnnotationPresent(SpecComment.class)) { + SpecComment comment = field.getAnnotation(SpecComment.class); + component = Component.literal(comment.value()); + } + + if (field.isAnnotationPresent(Tooltip.class)) { + Tooltip tooltip = field.getAnnotation(Tooltip.class); + Component c = Component.empty(); + + for (String comment : tooltip.value()) { + c.getSiblings().add(Component.literal(comment)); + } + + component = c; + } + + return component; + } + + /** + * A helper method to build translation keys for config screens, fields etc + * + * @param baseConfig The {@link AbstractConfig} being processed + * @param currentConfig The field being processed + * @param fieldName The raw name of the field + * @return A {@link Component} with the new translation key + */ + private static Component getTranslationKey(AbstractConfig baseConfig, Object currentConfig, String fieldName) { + String baseKey = "cl.config." + baseConfig.getClass().getSimpleName().toLowerCase(); + + if (currentConfig != baseConfig) { + baseKey += "." + currentConfig.getClass().getSimpleName().toLowerCase(); + } + + if (fieldName != null) { + baseKey += "." + fieldName.toLowerCase(); + } + + return Component.translatable(baseKey); + } + + /** + * Helper method to write changes values back to the config using reflection + * + * @param value The new value of the field + * @param field The field that needs to be updated + * @param config The object used to invoke the field for updating + */ + private static void saveFieldValue(Object value, Field field, Object config) { + try { + if (value instanceof List && !field.getType().equals(List.class)) { + List newList = (List)field.getType().getDeclaredConstructor().newInstance(); + newList.addAll((List)value); + field.set(config, newList); + } else { + field.set(config, value); + } + } catch (Exception e) { + CraterConstants.LOG.error("Failed to write config field {}", field.getName(), e); + } + } + + /** + * Safety method to prevent config screens from corrupting configs. In some edge cases, the config gui + * can generate invalid values, that cause the config saving to fail, and then save an empty file. + * This method makes a backup of the config before writing to it, and restores the backup if it fails + * + * @param config The {@link AbstractConfig} being saved + */ + private static void safeSaveConfig(AbstractConfig config) { + File configPath = config.getConfigPath(); + Path backupPath = configPath.toPath().resolveSibling(configPath.getName() + ".bak"); + + try { + Files.copy(configPath.toPath(), backupPath, StandardCopyOption.REPLACE_EXISTING); + config.saveConfig(config); + Files.deleteIfExists(backupPath); + } catch (Exception e) { + Minecraft.getInstance().getToastManager().addToast( + new SystemToast( + SystemToast.SystemToastId.PACK_LOAD_FAILURE, + Component.literal("Failed To Save Config"), + Component.literal("Restoring Backup Copy. Check log for details")) + ); + CraterConstants.LOG.error("Failed to save config, restoring backup", e); + try { + Files.copy(backupPath, configPath.toPath(), StandardCopyOption.REPLACE_EXISTING); + config.configReloaded(); + } catch (Exception restoreError) { + CraterConstants.LOG.error("Failed to restore config backup", restoreError); + } + } + } + +} diff --git a/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/CraterConfigScreen.java b/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/CraterConfigScreen.java index 01a7f94..38d0cc6 100644 --- a/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/CraterConfigScreen.java +++ b/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/CraterConfigScreen.java @@ -37,6 +37,7 @@ import java.util.function.Supplier; /** * @author HypherionSA */ +@Deprecated(forRemoval = true, since = "2.1.3") public class CraterConfigScreen extends Screen { public static final float SCROLLBAR_BOTTOM_COLOR = .5f; public static final float SCROLLBAR_TOP_COLOR = .67f; diff --git a/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/AbstractConfigWidget.java b/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/AbstractConfigWidget.java index ed34f6b..c46ea5d 100644 --- a/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/AbstractConfigWidget.java +++ b/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/AbstractConfigWidget.java @@ -10,6 +10,7 @@ import net.minecraft.client.gui.components.EditBox; * Copied from Cloth Config Lite * ... */ +@Deprecated(forRemoval = true, since = "2.1.3") public class AbstractConfigWidget extends BaseWidget { public static final int buttonWidth = 200; diff --git a/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/BaseWidget.java b/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/BaseWidget.java index 0583e61..088d2a5 100644 --- a/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/BaseWidget.java +++ b/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/BaseWidget.java @@ -13,6 +13,7 @@ import net.minecraft.network.chat.TextColor; * Copied from Cloth Config Lite * ... */ +@Deprecated(forRemoval = true, since = "2.1.3") public class BaseWidget extends Option { public static final int resetButtonOffset = 48; diff --git a/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/ClothConfigButtonEntry.java b/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/ClothConfigButtonEntry.java new file mode 100644 index 0000000..1e4db60 --- /dev/null +++ b/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/ClothConfigButtonEntry.java @@ -0,0 +1,128 @@ +package com.hypherionmc.craterlib.client.gui.config.widgets; + +import com.mojang.blaze3d.platform.Window; +import me.shedaniel.clothconfig2.api.AbstractConfigListEntry; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * @author HypherionSA + * A Custom Cloth Config GUI entry to allow buttons to be added to the GUI + */ +public class ClothConfigButtonEntry extends AbstractConfigListEntry { + + private final Button button; + private final Button deleteButton; + private final Component displayName; + private final boolean hasDeleteButton; + private final boolean wasEdited; + + /** + * Create a new Cloth Button Entry, that will have no delete button + * + * @param displayName The Display Name that will be used for the field + * @param fieldName The Display Name that will be used on the button + * @param onPress The Action to perform when the button was pressed + */ + public ClothConfigButtonEntry(Component displayName, Component fieldName, @Nullable Button.OnPress onPress) { + this(displayName, fieldName, onPress, null, false); + } + + /*** + * Create a new Cloth Button Entry, with optional delete button + * + * @param displayName The Display Name that will be used for the field + * @param fieldName The Display Name that will be used on the button + * @param onPress The Action to perform when the button was pressed + * @param deletePress The Action to perform when the delete button is pressed. If this is null, the button is disabled + * @param wasEdited Was a change made to the field this button belongs to. This is to tell cloth to enable the save button + */ + public ClothConfigButtonEntry(Component displayName, Component fieldName, Button.OnPress onPress, @Nullable Button.OnPress deletePress, boolean wasEdited) { + super(fieldName, false); + this.hasDeleteButton = deletePress != null; + this.wasEdited = wasEdited; + + int mainButtonWidth = hasDeleteButton ? 75 : 100; + this.button = Button.builder(fieldName, onPress).size(mainButtonWidth, 20).pos(0, 0).build(); + this.deleteButton = deletePress != null ? Button.builder(Component.literal("X"), deletePress).size(20, 20).pos(0, 0).build() : null; + this.displayName = displayName; + } + + @Override + public void render(GuiGraphics matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean isHovered, float delta) { + Window window = Minecraft.getInstance().getWindow(); + Component displayedFieldName = displayName; + if (Minecraft.getInstance().font.isBidirectional()) { + matrices.drawString(Minecraft.getInstance().font, displayedFieldName.getVisualOrderText(), window.getGuiScaledWidth() - x - Minecraft.getInstance().font.width(displayedFieldName), y + 6, 16777215); + this.button.setX(x); + if (hasDeleteButton) { + this.deleteButton.setX(x + this.button.getWidth() + 4); + } + } else { + matrices.drawString(Minecraft.getInstance().font, displayedFieldName.getVisualOrderText(), x, y + 6, this.getPreferredTextColor()); + if (hasDeleteButton) { + this.button.setX(x + entryWidth - this.button.getWidth() - 24); + this.deleteButton.setX(x + entryWidth - 20); + } else { + this.button.setX(x + entryWidth - this.button.getWidth()); + } + } + + button.setY(y + (entryHeight - 20) / 2); + button.render(matrices, mouseX, mouseY, delta); + + if (hasDeleteButton) { + deleteButton.setY(y + (entryHeight - 20) / 2); + deleteButton.render(matrices, mouseX, mouseY, delta); + } + } + + @Override + public Void getValue() { return null; } + + @Override + public Optional getDefaultValue() { return Optional.empty(); } + + @Override + public void save() {} + + @NotNull + @Override + public List children() { + ArrayList children = new ArrayList<>(); + children.add(button); + + if (hasDeleteButton) { + children.add(deleteButton); + } + + return children; + } + + @Override + public List narratables() { + ArrayList children = new ArrayList<>(); + children.add(button); + + if (hasDeleteButton) { + children.add(deleteButton); + } + + return children; + } + + @Override + public boolean isEdited() { + return wasEdited; + } +} \ No newline at end of file diff --git a/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/InternalConfigButton.java b/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/InternalConfigButton.java index 2478c7d..d93386f 100644 --- a/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/InternalConfigButton.java +++ b/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/InternalConfigButton.java @@ -10,6 +10,7 @@ import net.minecraft.network.chat.Component; /** * @author HypherionSA */ +@Deprecated(forRemoval = true, since = "2.1.3") public class InternalConfigButton extends AbstractButton { CraterConfigScreen screen; diff --git a/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/Option.java b/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/Option.java index f49ca68..3ac9f32 100644 --- a/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/Option.java +++ b/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/Option.java @@ -20,6 +20,7 @@ import java.util.function.Supplier; * Copied from Cloth Config Lite * ... */ +@Deprecated(forRemoval = true, since = "2.1.3") public abstract class Option extends AbstractContainerEventHandler { public Component text; diff --git a/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/SubConfigWidget.java b/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/SubConfigWidget.java index 056f32c..f97b5bc 100644 --- a/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/SubConfigWidget.java +++ b/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/SubConfigWidget.java @@ -12,6 +12,7 @@ import net.minecraft.network.chat.Component; /** * @author HypherionSA */ +@Deprecated(forRemoval = true, since = "2.1.3") public class SubConfigWidget extends AbstractConfigWidget { private final Object subConfig; diff --git a/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/TextConfigOption.java b/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/TextConfigOption.java index cc05cec..ef5f955 100644 --- a/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/TextConfigOption.java +++ b/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/TextConfigOption.java @@ -10,6 +10,7 @@ import java.util.function.Function; * Copied from Cloth Config Lite * ... */ +@Deprecated(forRemoval = true, since = "2.1.3") public class TextConfigOption extends AbstractConfigWidget { private final Function toString; diff --git a/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/ToggleButton.java b/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/ToggleButton.java index 03ea11d..c90b67d 100644 --- a/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/ToggleButton.java +++ b/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/ToggleButton.java @@ -10,6 +10,7 @@ import java.util.function.Function; * Copied from Cloth Config Lite * ... */ +@Deprecated(forRemoval = true, since = "2.1.3") public class ToggleButton extends AbstractConfigWidget { private final List options; diff --git a/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/WrappedEditBox.java b/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/WrappedEditBox.java index 42c0c0c..8c8282d 100644 --- a/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/WrappedEditBox.java +++ b/Common/src/main/java/com/hypherionmc/craterlib/client/gui/config/widgets/WrappedEditBox.java @@ -10,6 +10,7 @@ import org.jetbrains.annotations.NotNull; /** * @author HypherionSA */ +@Deprecated(forRemoval = true, since = "2.1.3") public class WrappedEditBox extends EditBox { public WrappedEditBox(Font font, int i, int j, int k, int l, @NotNull Component component) { diff --git a/Common/src/main/java/com/hypherionmc/craterlib/core/config/annotations/ClothScreen.java b/Common/src/main/java/com/hypherionmc/craterlib/core/config/annotations/ClothScreen.java new file mode 100644 index 0000000..bec46eb --- /dev/null +++ b/Common/src/main/java/com/hypherionmc/craterlib/core/config/annotations/ClothScreen.java @@ -0,0 +1,12 @@ +package com.hypherionmc.craterlib.core.config.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * @author HypherionSA + * Allow mods to make use of the new Cloth Config based Config Screens + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface ClothScreen { +} diff --git a/Common/src/main/java/com/hypherionmc/craterlib/nojang/client/gui/BridgedScreen.java b/Common/src/main/java/com/hypherionmc/craterlib/nojang/client/gui/BridgedScreen.java index c4bcc23..716b4c9 100644 --- a/Common/src/main/java/com/hypherionmc/craterlib/nojang/client/gui/BridgedScreen.java +++ b/Common/src/main/java/com/hypherionmc/craterlib/nojang/client/gui/BridgedScreen.java @@ -1,10 +1,7 @@ package com.hypherionmc.craterlib.nojang.client.gui; import lombok.RequiredArgsConstructor; -import net.minecraft.client.gui.screens.LevelLoadingScreen; -import net.minecraft.client.gui.screens.ReceivingLevelScreen; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.client.gui.screens.TitleScreen; +import net.minecraft.client.gui.screens.*; import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen; import net.minecraft.realms.RealmsScreen; @@ -29,6 +26,14 @@ public class BridgedScreen { return internal instanceof LevelLoadingScreen || internal instanceof ReceivingLevelScreen; } + public boolean isPauseScreen() { + return internal instanceof PauseScreen; + } + + public boolean isDisconnetedScreen() { + return internal instanceof DisconnectedScreen; + } + public Screen toMojang() { return internal; } diff --git a/Common/src/main/java/com/hypherionmc/craterlib/nojang/world/entity/player/BridgedPlayer.java b/Common/src/main/java/com/hypherionmc/craterlib/nojang/world/entity/player/BridgedPlayer.java index 2241836..9bf7600 100644 --- a/Common/src/main/java/com/hypherionmc/craterlib/nojang/world/entity/player/BridgedPlayer.java +++ b/Common/src/main/java/com/hypherionmc/craterlib/nojang/world/entity/player/BridgedPlayer.java @@ -7,6 +7,7 @@ import lombok.RequiredArgsConstructor; import net.kyori.adventure.text.Component; import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.world.InteractionHand; import net.minecraft.world.entity.player.Player; import org.jetbrains.annotations.Nullable; @@ -49,6 +50,34 @@ public class BridgedPlayer { return BridgedBlockPos.of(internal.getOnPos()); } + public float getHealth() { + return internal.getHealth(); + } + + public float getMaxHealth() { + return internal.getMaxHealth(); + } + + public String getHeldItemMainHand() { + String value = "Nothing"; + + if (!internal.getItemInHand(InteractionHand.MAIN_HAND).isEmpty()) { + value = internal.getItemInHand(InteractionHand.MAIN_HAND).getDisplayName().getString(); + } + + return value; + } + + public String getHeldItemOffHand() { + String value = "Nothing"; + + if (!internal.getItemInHand(InteractionHand.OFF_HAND).isEmpty()) { + value = internal.getItemInHand(InteractionHand.OFF_HAND).getDisplayName().getString(); + } + + return value; + } + @Nullable public ServerGamePacketListenerImpl getConnection() { if (isServerPlayer()) { diff --git a/Common/src/main/resources/assets/craterlib/lang/en_us.json b/Common/src/main/resources/assets/craterlib/lang/en_us.json index 03115d9..7dbfff1 100644 --- a/Common/src/main/resources/assets/craterlib/lang/en_us.json +++ b/Common/src/main/resources/assets/craterlib/lang/en_us.json @@ -4,5 +4,9 @@ "t.clc.cancel_discard": "Discard", "t.clc.quit_config": "Unsaved Changes", "t.clc.quit_config_sure": "You have unsaved config changes. Are you sure you want to discard them?", - "t.clc.quit_discard": "Quit & Discard" + "t.clc.quit_discard": "Quit & Discard", + + "cl.buttons.edit": "Edit", + "cl.buttons.add_entry": "+ Add Entry", + "cl.buttons.entry": "Entry %s" } diff --git a/Fabric/build.gradle b/Fabric/build.gradle index b3da913..b64c1d0 100644 --- a/Fabric/build.gradle +++ b/Fabric/build.gradle @@ -12,6 +12,7 @@ dependencies { stupidRemapArch("dev.ftb.mods:ftb-essentials:${ftb_essentials}") stupidRemapArch("dev.ftb.mods:ftb-ranks:${ftb_ranks}") + modImplementation("me.shedaniel.cloth:cloth-config-fabric:${cloth_config}") modImplementation "maven.modrinth:fabrictailor:${fabrictailor}" modImplementation "maven.modrinth:vanish:${vanish}" @@ -116,8 +117,8 @@ publisher { setVersionType("release") setChangelog(rootProject.file("changelog.md")) setProjectVersion("${minecraft_version}-${project.version}") - setDisplayName("[FABRIC/QUILT 1.21.3] CraterLib - ${project.version}") - setGameVersions("1.21.3") + setDisplayName("[FABRIC/QUILT 1.21.3/4] CraterLib - ${project.version}") + setGameVersions("1.21.3", "1.21.4") setLoaders("fabric", "quilt") setArtifact(remapJar) setCurseEnvironment("both") @@ -125,9 +126,11 @@ publisher { modrinthDepends { required("fabric-api") + optional("cloth-config", "modmenu") } curseDepends { required("fabric-api") + optional("cloth-config", "modmenu") } } \ No newline at end of file diff --git a/Fabric/src/main/java/com/hypherionmc/craterlib/CraterLibModMenuIntegration.java b/Fabric/src/main/java/com/hypherionmc/craterlib/CraterLibModMenuIntegration.java index 5b34ff1..b4f4429 100644 --- a/Fabric/src/main/java/com/hypherionmc/craterlib/CraterLibModMenuIntegration.java +++ b/Fabric/src/main/java/com/hypherionmc/craterlib/CraterLibModMenuIntegration.java @@ -1,8 +1,12 @@ package com.hypherionmc.craterlib; +import com.hypherionmc.craterlib.client.gui.config.ClothConfigScreenBuilder; import com.hypherionmc.craterlib.client.gui.config.CraterConfigScreen; +import com.hypherionmc.craterlib.core.config.AbstractConfig; import com.hypherionmc.craterlib.core.config.ConfigController; +import com.hypherionmc.craterlib.core.config.annotations.ClothScreen; import com.hypherionmc.craterlib.core.config.annotations.NoConfigScreen; +import com.hypherionmc.craterlib.core.platform.ModloaderEnvironment; import com.terraformersmc.modmenu.api.ConfigScreenFactory; import com.terraformersmc.modmenu.api.ModMenuApi; @@ -19,8 +23,14 @@ public class CraterLibModMenuIntegration implements ModMenuApi { Map> configScreens = new HashMap<>(); ConfigController.getWatchedConfigs().forEach((conf, watcher) -> { - if (!conf.getClass().isAnnotationPresent(NoConfigScreen.class)) { - configScreens.put(watcher.getLeft().getModId(), screen -> new CraterConfigScreen(watcher.getLeft(), screen)); + AbstractConfig config = watcher.getLeft(); + if (config.getClass().isAnnotationPresent(NoConfigScreen.class)) + return; + + if (watcher.getLeft().getClass().isAnnotationPresent(ClothScreen.class) && (ModloaderEnvironment.INSTANCE.isModLoaded("cloth_config") || ModloaderEnvironment.INSTANCE.isModLoaded("cloth-config") || ModloaderEnvironment.INSTANCE.isModLoaded("clothconfig"))) { + configScreens.put(config.getModId(), screen -> ClothConfigScreenBuilder.buildConfigScreen(config, screen)); + } else { + //configScreens.put(config.getModId(), screen -> new CraterConfigScreen(config, screen)); } }); diff --git a/Forge/build.gradle b/Forge/build.gradle index 9f2ac13..6489d4c 100644 --- a/Forge/build.gradle +++ b/Forge/build.gradle @@ -5,6 +5,8 @@ dependencies { // Compat // NOT AVAILABLE ON FORGE modImplementation("maven.modrinth:vanishmod:${vanishmod}") + modImplementation("me.shedaniel.cloth:cloth-config-forge:${cloth_config}") + // Do not edit or remove implementation project(":Common") } @@ -113,4 +115,12 @@ publisher { setArtifact(remapJar) setCurseEnvironment("both") setIsManualRelease(true) + + curseDepends { + optional("cloth-config") + } + + modrinthDepends { + optional("cloth-config") + } } \ No newline at end of file diff --git a/Forge/src/main/java/com/hypherionmc/craterlib/mixin/ConfigScreenHandlerMixin.java b/Forge/src/main/java/com/hypherionmc/craterlib/mixin/ConfigScreenHandlerMixin.java index 927bd23..1603d91 100644 --- a/Forge/src/main/java/com/hypherionmc/craterlib/mixin/ConfigScreenHandlerMixin.java +++ b/Forge/src/main/java/com/hypherionmc/craterlib/mixin/ConfigScreenHandlerMixin.java @@ -29,13 +29,14 @@ public class ConfigScreenHandlerMixin { @Inject(at = @At("RETURN"), method = "getScreenFactoryFor", cancellable = true, remap = false) private static void injectConfigScreen(IModInfo selectedMod, CallbackInfoReturnable>> cir) { ConfigController.getMonitoredConfigs().forEach((conf, watcher) -> { - if (!conf.getClass().isAnnotationPresent(NoConfigScreen.class)) { - ModuleConfig config = (ModuleConfig) conf; - if (config.getModId().equals(selectedMod.getModId())) { - cir.setReturnValue( - Optional.of((minecraft, screen) -> new CraterConfigScreen(config, screen)) - ); - } + AbstractConfig config = watcher.getLeft(); + if (config.getClass().isAnnotationPresent(NoConfigScreen.class)) + return; + + if (watcher.getLeft().getClass().isAnnotationPresent(ClothScreen.class) && (ModloaderEnvironment.INSTANCE.isModLoaded("cloth_config") || ModloaderEnvironment.INSTANCE.isModLoaded("cloth-config") || ModloaderEnvironment.INSTANCE.isModLoaded("clothconfig"))) { + ModList.get().getModContainerById(config.getModId()).ifPresent(c -> c.registerExtensionPoint(IConfigScreenFactory.class, ((minecraft, screen) -> ClothConfigScreenBuilder.buildConfigScreen(config, screen)))); + } else { + //ModList.get().getModContainerById(config.getModId()).ifPresent(c -> c.registerExtensionPoint(IConfigScreenFactory.class, ((minecraft, screen) -> new CraterConfigScreen(config, screen)))); } }); } diff --git a/NeoForge/build.gradle b/NeoForge/build.gradle index bc934ff..486d3f8 100644 --- a/NeoForge/build.gradle +++ b/NeoForge/build.gradle @@ -7,6 +7,8 @@ dependencies { stupidRemapArch("dev.ftb.mods:ftb-essentials-neoforge:${ftb_essentials}") stupidRemapArch("dev.ftb.mods:ftb-ranks-neoforge:${ftb_ranks}") + modImplementation("me.shedaniel.cloth:cloth-config-neoforge:${cloth_config}") + // Do not edit or remove implementation project(":Common") } @@ -114,10 +116,18 @@ publisher { setVersionType("release") setChangelog(rootProject.file("changelog.md")) setProjectVersion("${minecraft_version}-${project.version}") - setDisplayName("[NeoForge 1.21.3] CraterLib - ${project.version}") - setGameVersions("1.21.3") + setDisplayName("[NeoForge 1.21.3/1.21.4] CraterLib - ${project.version}") + setGameVersions("1.21.3", "1.21.4") setLoaders("neoforge") setArtifact(remapJar) setCurseEnvironment("both") setIsManualRelease(true) + + curseDepends { + optional("cloth-config") + } + + modrinthDepends { + optional("cloth-config") + } } \ No newline at end of file diff --git a/NeoForge/src/main/java/com/hypherionmc/craterlib/client/NeoForgeClientHelper.java b/NeoForge/src/main/java/com/hypherionmc/craterlib/client/NeoForgeClientHelper.java index e7d9013..eacd3a3 100644 --- a/NeoForge/src/main/java/com/hypherionmc/craterlib/client/NeoForgeClientHelper.java +++ b/NeoForge/src/main/java/com/hypherionmc/craterlib/client/NeoForgeClientHelper.java @@ -1,12 +1,15 @@ package com.hypherionmc.craterlib.client; import com.hypherionmc.craterlib.api.events.client.LateInitEvent; +import com.hypherionmc.craterlib.client.gui.config.ClothConfigScreenBuilder; import com.hypherionmc.craterlib.client.gui.config.CraterConfigScreen; import com.hypherionmc.craterlib.core.config.AbstractConfig; import com.hypherionmc.craterlib.core.config.ConfigController; +import com.hypherionmc.craterlib.core.config.annotations.ClothScreen; import com.hypherionmc.craterlib.core.config.annotations.NoConfigScreen; import com.hypherionmc.craterlib.core.event.CraterEventBus; import com.hypherionmc.craterlib.core.platform.ClientPlatform; +import com.hypherionmc.craterlib.core.platform.ModloaderEnvironment; import com.hypherionmc.craterlib.nojang.client.BridgedMinecraft; import com.hypherionmc.craterlib.nojang.client.BridgedOptions; import com.hypherionmc.craterlib.nojang.client.multiplayer.BridgedClientLevel; @@ -52,9 +55,14 @@ public class NeoForgeClientHelper implements ClientPlatform { CraterEventBus.INSTANCE.postEvent(event); ConfigController.getWatchedConfigs().forEach((conf, watcher) -> { - if (!conf.getClass().isAnnotationPresent(NoConfigScreen.class)) { - AbstractConfig config = watcher.getLeft(); - ModList.get().getModContainerById(config.getModId()).ifPresent(c -> c.registerExtensionPoint(IConfigScreenFactory.class, ((minecraft, screen) -> new CraterConfigScreen(config, screen)))); + AbstractConfig config = watcher.getLeft(); + if (config.getClass().isAnnotationPresent(NoConfigScreen.class)) + return; + + if (watcher.getLeft().getClass().isAnnotationPresent(ClothScreen.class) && (ModloaderEnvironment.INSTANCE.isModLoaded("cloth_config") || ModloaderEnvironment.INSTANCE.isModLoaded("cloth-config") || ModloaderEnvironment.INSTANCE.isModLoaded("clothconfig"))) { + ModList.get().getModContainerById(config.getModId()).ifPresent(c -> c.registerExtensionPoint(IConfigScreenFactory.class, ((minecraft, screen) -> ClothConfigScreenBuilder.buildConfigScreen(config, screen)))); + } else { + //ModList.get().getModContainerById(config.getModId()).ifPresent(c -> c.registerExtensionPoint(IConfigScreenFactory.class, ((minecraft, screen) -> new CraterConfigScreen(config, screen)))); } }); } diff --git a/changelog.md b/changelog.md index 09222a4..c4a47e5 100644 --- a/changelog.md +++ b/changelog.md @@ -1,12 +1,9 @@ -**New Features**: - -- Paper Support. Currently only available on [NightBloom](https://nightbloom.cc/project/craterlib/files?loader=paper) -- Added APIs for working with FTB Ranks and LuckPerms groups - **Bug Fixes**: -- Fixed Vanish compact API being swapped +- Added a workaround for LuckPerms turning players into ghost players +- Fixed Missing Getters on LuckPerms events -**Changes**: +**New Features**: -- Config library now logs which line of the config the error is on \ No newline at end of file +- Swapped Built In Config screen for a Cloth Config System, so client side mods can have in-game configs +- Added new APIs for Simple RPC (V4 rewrite) \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 812aff8..c4e7992 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,8 +1,8 @@ #Project version_major=2 version_minor=1 -version_patch=2 -version_build=1 +version_patch=3 +version_build=0 #Mod mod_author=HypherionSA @@ -29,6 +29,7 @@ lombok=1.18.32 adventure=4.17.0 rpc_sdk=1.0 discord_formatter=2.0.0 +cloth_config=17.0.144 # Mod Dependencies fabrictailor=2.3.1