[DEV] New Cloth Config GUIs, new nojang apis, and bug fixes

This commit is contained in:
2025-01-14 17:14:57 +02:00
parent 864baeb6c5
commit 8a46899769
290 changed files with 6213 additions and 459 deletions

View File

@@ -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<Enum> enumClass = (Class<Enum>)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<String>) list)
.setTooltip(getToolTip(field))
.setSaveConsumer(newValue -> saveFieldValue(new ArrayList<>(newValue), field, config))
.setDefaultValue((List<String>) list).build());
// Int List
} else if (elementType.equals(Integer.class)) {
configCategory.addEntry(entryBuilder.startIntList(getTranslationKey(baseConfig, config, field.getName()), (List<Integer>) list)
.setTooltip(getToolTip(field))
.setSaveConsumer(newValue -> saveFieldValue(new ArrayList<>(newValue), field, config))
.setDefaultValue((List<Integer>) list).build());
// Long List
} else if (elementType.equals(Long.class)) {
configCategory.addEntry(entryBuilder.startLongList(getTranslationKey(baseConfig, config, field.getName()), (List<Long>) list)
.setTooltip(getToolTip(field))
.setSaveConsumer(newValue -> saveFieldValue(new ArrayList<>(newValue), field, config))
.setDefaultValue((List<Long>) list).build());
// Float List
} else if (elementType.equals(Float.class)) {
configCategory.addEntry(entryBuilder.startFloatList(getTranslationKey(baseConfig, config, field.getName()), (List<Float>) list)
.setTooltip(getToolTip(field))
.setSaveConsumer(newValue -> saveFieldValue(new ArrayList<>(newValue), field, config))
.setDefaultValue((List<Float>) list).build());
// Double List
} else if (elementType.equals(Double.class)) {
configCategory.addEntry(entryBuilder.startDoubleList(getTranslationKey(baseConfig, config, field.getName()), (List<Double>) list)
.setTooltip(getToolTip(field))
.setSaveConsumer(newValue -> saveFieldValue(new ArrayList<>(newValue), field, config))
.setDefaultValue((List<Double>) 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().getToasts().addToast(
new SystemToast(
SystemToast.SystemToastIds.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);
}
}
}
}

View File

@@ -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;

View File

@@ -10,6 +10,7 @@ import net.minecraft.client.gui.components.EditBox;
* Copied from Cloth Config Lite
* <a href="https://github.com/shedaniel/cloth-config-lite/blob/1.17/src/main/java/me/shedaniel/clothconfiglite/impl/option/AbstractWidgetOption.java">...</a>
*/
@Deprecated(forRemoval = true, since = "2.1.3")
public class AbstractConfigWidget<T, W extends AbstractWidget> extends BaseWidget<T> {
public static final int buttonWidth = 200;

View File

@@ -13,6 +13,7 @@ import net.minecraft.network.chat.TextColor;
* Copied from Cloth Config Lite
* <a href="https://github.com/shedaniel/cloth-config-lite/blob/1.17/src/main/java/me/shedaniel/clothconfiglite/impl/option/BaseOption.java">...</a>
*/
@Deprecated(forRemoval = true, since = "2.1.3")
public class BaseWidget<T> extends Option<T> {
public static final int resetButtonOffset = 48;

View File

@@ -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<Void> {
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<Void> getDefaultValue() { return Optional.empty(); }
@Override
public void save() {}
@NotNull
@Override
public List<? extends GuiEventListener> children() {
ArrayList<GuiEventListener> children = new ArrayList<>();
children.add(button);
if (hasDeleteButton) {
children.add(deleteButton);
}
return children;
}
@Override
public List<? extends NarratableEntry> narratables() {
ArrayList<NarratableEntry> children = new ArrayList<>();
children.add(button);
if (hasDeleteButton) {
children.add(deleteButton);
}
return children;
}
@Override
public boolean isEdited() {
return wasEdited;
}
}

View File

@@ -11,6 +11,7 @@ import org.jetbrains.annotations.NotNull;
/**
* @author HypherionSA
*/
@Deprecated(forRemoval = true, since = "2.1.3")
public class InternalConfigButton extends AbstractButton {
CraterConfigScreen screen;

View File

@@ -20,6 +20,7 @@ import java.util.function.Supplier;
* Copied from Cloth Config Lite
* <a href="https://github.com/shedaniel/cloth-config-lite/blob/1.17/src/main/java/me/shedaniel/clothconfiglite/impl/option/Option.java">...</a>
*/
@Deprecated(forRemoval = true, since = "2.1.3")
public abstract class Option<T> extends AbstractContainerEventHandler {
public Component text;

View File

@@ -12,6 +12,7 @@ import net.minecraft.network.chat.Component;
/**
* @author HypherionSA
*/
@Deprecated(forRemoval = true, since = "2.1.3")
public class SubConfigWidget<T> extends AbstractConfigWidget<T, Button> {
private final Object subConfig;

View File

@@ -10,6 +10,7 @@ import java.util.function.Function;
* Copied from Cloth Config Lite
* <a href="https://github.com/shedaniel/cloth-config-lite/blob/1.17/src/main/java/me/shedaniel/clothconfiglite/impl/option/TextFieldOption.java">...</a>
*/
@Deprecated(forRemoval = true, since = "2.1.3")
public class TextConfigOption<T> extends AbstractConfigWidget<T, WrappedEditBox> {
private final Function<T, String> toString;

View File

@@ -10,6 +10,7 @@ import java.util.function.Function;
* Copied from Cloth Config Lite
* <a href="https://github.com/shedaniel/cloth-config-lite/blob/1.17/src/main/java/me/shedaniel/clothconfiglite/impl/option/ToggleOption.java">...</a>
*/
@Deprecated(forRemoval = true, since = "2.1.3")
public class ToggleButton<T> extends AbstractConfigWidget<T, Button> {
private final List<T> options;

View File

@@ -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) {