[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

@@ -3,6 +3,8 @@ archivesBaseName = "${mod_name.replace(" ", "")}-Common-${minecraft_version}"
dependencies {
stupidRemapArch("dev.ftb.mods:ftb-ranks:${ftb_ranks}")
stupidRemapArch("me.shedaniel.cloth:cloth-config:${cloth_config}")
}
shadowJar {

View File

@@ -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;
@@ -138,9 +139,14 @@ public class CraterCommand {
}
private boolean checkPermission(CommandSourceStack stack) {
try {
if (!ModloaderEnvironment.INSTANCE.isModLoaded("luckperms") || !(stack.getEntity() instanceof Player) || luckPermNode.isEmpty())
return stack.hasPermission(this.permLevel);
return stack.hasPermission(this.permLevel);
} catch (Exception e) {
CraterConstants.LOG.error("Failed to check luckperms permissions", e);
return stack.hasPermission(this.permLevel);
}
return LuckPermsCompat.INSTANCE.hasPermission((ServerPlayer) stack.getEntity(), this.luckPermNode) || stack.hasPermission(this.permLevel);
}

View File

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

View File

@@ -0,0 +1,411 @@
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 net.minecraft.network.chat.TextComponent;
import net.minecraft.network.chat.TranslatableComponent;
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(new TextComponent("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(
new TranslatableComponent("cl.buttons.entry", (i + 1)),
new TranslatableComponent("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(
new TextComponent(""),
new TranslatableComponent("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(new TextComponent("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(
new TranslatableComponent("cl.config." + baseConfig.getClass().getSimpleName().toLowerCase() + "." + field.getName().toLowerCase()),
new TranslatableComponent("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()),
new TranslatableComponent("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 = new TextComponent("");
if (field.isAnnotationPresent(SpecComment.class)) {
SpecComment comment = field.getAnnotation(SpecComment.class);
component = new TextComponent(comment.value());
}
if (field.isAnnotationPresent(Tooltip.class)) {
Tooltip tooltip = field.getAnnotation(Tooltip.class);
Component c = new TextComponent("");
for (String comment : tooltip.value()) {
c.getSiblings().add(new TextComponent(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 new TranslatableComponent(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,
new TextComponent("Failed To Save Config"),
new TextComponent("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

@@ -38,6 +38,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.TextComponent;
* 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,129 @@
package com.hypherionmc.craterlib.client.gui.config.widgets;
import com.mojang.blaze3d.platform.Window;
import com.mojang.blaze3d.vertex.PoseStack;
import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
import net.minecraft.client.Minecraft;
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 net.minecraft.network.chat.TextComponent;
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 = new Button(0, 0, mainButtonWidth, 20, fieldName, onPress);
this.deleteButton = deletePress != null ? new Button(0, 0, 20, 20, new TextComponent("X"), deletePress) : null;
this.displayName = displayName;
}
@Override
public void render(PoseStack 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()) {
drawString(matrices, Minecraft.getInstance().font, displayedFieldName.getVisualOrderText(), window.getGuiScaledWidth() - x - Minecraft.getInstance().font.width(displayedFieldName), y + 6, 16777215);
this.button.x = x;
if (hasDeleteButton) {
this.deleteButton.x = x + this.button.getWidth() + 4;
}
} else {
drawString(matrices, Minecraft.getInstance().font, displayedFieldName.getVisualOrderText(), x, y + 6, this.getPreferredTextColor());
if (hasDeleteButton) {
this.button.x = x + entryWidth - this.button.getWidth() - 24;
this.deleteButton.x = x + entryWidth - 20;
} else {
this.button.x = x + entryWidth - this.button.getWidth();
}
}
button.y = y + (entryHeight - 20) / 2;
button.render(matrices, mouseX, mouseY, delta);
if (hasDeleteButton) {
deleteButton.y = 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

@@ -12,6 +12,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.TranslatableComponent;
/**
* @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

@@ -11,6 +11,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) {

View File

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

View File

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

View File

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

View File

@@ -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"
}

View File

@@ -9,6 +9,7 @@ dependencies {
exclude(group: "net.fabricmc.fabric-api")
}
modImplementation("me.shedaniel.cloth:cloth-config-fabric:${cloth_config}")
modImplementation "maven.modrinth:fabrictailor:${fabrictailor}"
modImplementation "maven.modrinth:vanish:${vanish}"
@@ -122,9 +123,11 @@ publisher {
modrinthDepends {
required("fabric-api")
optional("cloth-config", "modmenu")
}
curseDepends {
required("fabric-api")
optional("cloth-config", "modmenu")
}
}

View File

@@ -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<String, ConfigScreenFactory<?>> 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));
}
});

View File

@@ -5,6 +5,8 @@ dependencies {
// Compat
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")
}
}

View File

@@ -1,9 +1,12 @@
package com.hypherionmc.craterlib.mixin;
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 net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import net.minecraftforge.client.ConfigGuiHandler;
@@ -29,9 +32,16 @@ public class ConfigScreenHandlerMixin {
@Inject(at = @At("RETURN"), method = "getGuiFactoryFor", cancellable = true, remap = false)
private static void injectConfigScreen(IModInfo selectedMod, CallbackInfoReturnable<Optional<BiFunction<Minecraft, Screen, Screen>>> cir) {
ConfigController.getWatchedConfigs().forEach((conf, watcher) -> {
if (!conf.getClass().isAnnotationPresent(NoConfigScreen.class)) {
AbstractConfig config = watcher.getLeft();
if (config.getModId().equals(selectedMod.getModId())) {
AbstractConfig config = watcher.getLeft();
if (config.getClass().isAnnotationPresent(NoConfigScreen.class))
return;
if (config.getModId().equals(selectedMod.getModId())) {
if (watcher.getLeft().getClass().isAnnotationPresent(ClothScreen.class) && ModloaderEnvironment.INSTANCE.isModLoaded("cloth_config")) {
cir.setReturnValue(
Optional.of((minecraft, screen) -> ClothConfigScreenBuilder.buildConfigScreen(config, screen))
);
} else {
cir.setReturnValue(
Optional.of((minecraft, screen) -> new CraterConfigScreen(config, screen))
);

View File

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

View File

@@ -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
@@ -26,6 +26,7 @@ lombok=1.18.32
adventure=4.17.0
rpc_sdk=1.0
discord_formatter=2.0.0
cloth_config=6.5.102
# Mod Dependencies
ftb_ranks=1802.1.11-build.71

View File

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

View File

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

View File

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

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

@@ -36,6 +36,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 com.mojang.blaze3d.vertex.PoseStack;
import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
import net.minecraft.client.Minecraft;
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 = new Button(0, 0, mainButtonWidth, 20, fieldName, onPress);
this.deleteButton = deletePress != null ? new Button(0, 0, 20, 20, Component.literal("X"), deletePress) : null;
this.displayName = displayName;
}
@Override
public void render(PoseStack 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()) {
drawString(matrices, Minecraft.getInstance().font, displayedFieldName.getVisualOrderText(), window.getGuiScaledWidth() - x - Minecraft.getInstance().font.width(displayedFieldName), y + 6, 16777215);
this.button.x = x;
if (hasDeleteButton) {
this.deleteButton.x = x + this.button.getWidth() + 4;
}
} else {
drawString(matrices, Minecraft.getInstance().font, displayedFieldName.getVisualOrderText(), x, y + 6, this.getPreferredTextColor());
if (hasDeleteButton) {
this.button.x = x + entryWidth - this.button.getWidth() - 24;
this.deleteButton.x = x + entryWidth - 20;
} else {
this.button.x = x + entryWidth - this.button.getWidth();
}
}
button.y = y + (entryHeight - 20) / 2;
button.render(matrices, mouseX, mouseY, delta);
if (hasDeleteButton) {
deleteButton.y = 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) {

View File

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

View File

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

View File

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

View File

@@ -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"
}

View File

@@ -11,6 +11,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}"
@@ -124,9 +125,11 @@ publisher {
modrinthDepends {
required("fabric-api")
optional("cloth-config", "modmenu")
}
curseDepends {
required("fabric-api")
optional("cloth-config", "modmenu")
}
}

View File

@@ -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<String, ConfigScreenFactory<?>> 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));
}
});

View File

@@ -8,6 +8,8 @@ dependencies {
stupidRemapArch("dev.ftb.mods:ftb-essentials-forge:${ftb_essentials}")
stupidRemapArch("dev.ftb.mods:ftb-ranks-forge:${ftb_ranks}")
modImplementation("me.shedaniel.cloth:cloth-config-forge:${cloth_config}")
// Do not edit or remove
implementation project(":Common")
}
@@ -116,4 +118,12 @@ publisher {
setArtifact(remapJar)
setCurseEnvironment("both")
setIsManualRelease(true)
curseDepends {
optional("cloth-config")
}
modrinthDepends {
optional("cloth-config")
}
}

View File

@@ -1,9 +1,12 @@
package com.hypherionmc.craterlib.mixin;
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 net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import net.minecraftforge.client.ConfigScreenHandler;
@@ -29,9 +32,16 @@ public class ConfigScreenHandlerMixin {
@Inject(at = @At("RETURN"), method = "getScreenFactoryFor", cancellable = true, remap = false)
private static void injectConfigScreen(IModInfo selectedMod, CallbackInfoReturnable<Optional<BiFunction<Minecraft, Screen, Screen>>> cir) {
ConfigController.getWatchedConfigs().forEach((conf, watcher) -> {
if (!conf.getClass().isAnnotationPresent(NoConfigScreen.class)) {
AbstractConfig config = watcher.getLeft();
if (config.getModId().equals(selectedMod.getModId())) {
AbstractConfig config = watcher.getLeft();
if (config.getClass().isAnnotationPresent(NoConfigScreen.class))
return;
if (config.getModId().equals(selectedMod.getModId())) {
if (watcher.getLeft().getClass().isAnnotationPresent(ClothScreen.class) && ModloaderEnvironment.INSTANCE.isModLoaded("cloth_config")) {
cir.setReturnValue(
Optional.of((minecraft, screen) -> ClothConfigScreenBuilder.buildConfigScreen(config, screen))
);
} else {
cir.setReturnValue(
Optional.of((minecraft, screen) -> new CraterConfigScreen(config, screen))
);

View File

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

View File

@@ -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
@@ -26,6 +26,7 @@ lombok=1.18.32
adventure=4.17.0
rpc_sdk=1.0
discord_formatter=2.0.0
cloth_config=8.3.134
# Mod Dependencies
ftb_ranks=1902.1.16-build.114

View File

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

View File

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

View File

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

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

@@ -36,6 +36,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 com.mojang.blaze3d.vertex.PoseStack;
import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
import net.minecraft.client.Minecraft;
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(PoseStack 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()) {
drawString(matrices, 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 {
drawString(matrices, 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) {

View File

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

View File

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

View File

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

View File

@@ -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"
}

View File

@@ -11,6 +11,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}"
@@ -124,9 +125,11 @@ publisher {
modrinthDepends {
required("fabric-api")
optional("cloth-config", "modmenu")
}
curseDepends {
required("fabric-api")
optional("cloth-config", "modmenu")
}
}

View File

@@ -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<String, ConfigScreenFactory<?>> 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));
}
});

View File

@@ -8,6 +8,8 @@ dependencies {
stupidRemapArch("dev.ftb.mods:ftb-essentials-forge:${ftb_essentials}")
stupidRemapArch("dev.ftb.mods:ftb-ranks-forge:${ftb_ranks}")
modImplementation("me.shedaniel.cloth:cloth-config-forge:${cloth_config}")
// Do not edit or remove
implementation project(":Common")
}
@@ -116,4 +118,12 @@ publisher {
setArtifact(remapJar)
setCurseEnvironment("both")
setIsManualRelease(true)
curseDepends {
optional("cloth-config")
}
modrinthDepends {
optional("cloth-config")
}
}

View File

@@ -1,10 +1,12 @@
package com.hypherionmc.craterlib.mixin;
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.ModuleConfig;
import com.hypherionmc.craterlib.core.config.annotations.ClothScreen;
import com.hypherionmc.craterlib.core.config.annotations.NoConfigScreen;
import com.hypherionmc.craterlib.core.platform.ModloaderEnvironment;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import net.minecraftforge.client.ConfigScreenHandler;
@@ -30,9 +32,16 @@ public class ConfigScreenHandlerMixin {
@Inject(at = @At("RETURN"), method = "getScreenFactoryFor", cancellable = true, remap = false)
private static void injectConfigScreen(IModInfo selectedMod, CallbackInfoReturnable<Optional<BiFunction<Minecraft, Screen, Screen>>> cir) {
ConfigController.getWatchedConfigs().forEach((conf, watcher) -> {
if (!conf.getClass().isAnnotationPresent(NoConfigScreen.class)) {
AbstractConfig config = watcher.getLeft();
if (config.getModId().equals(selectedMod.getModId())) {
AbstractConfig config = watcher.getLeft();
if (config.getClass().isAnnotationPresent(NoConfigScreen.class))
return;
if (config.getModId().equals(selectedMod.getModId())) {
if (watcher.getLeft().getClass().isAnnotationPresent(ClothScreen.class) && ModloaderEnvironment.INSTANCE.isModLoaded("cloth_config")) {
cir.setReturnValue(
Optional.of((minecraft, screen) -> ClothConfigScreenBuilder.buildConfigScreen(config, screen))
);
} else {
cir.setReturnValue(
Optional.of((minecraft, screen) -> new CraterConfigScreen(config, screen))
);
@@ -40,5 +49,4 @@ public class ConfigScreenHandlerMixin {
}
});
}
}

View File

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

View File

@@ -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
@@ -26,6 +26,7 @@ lombok=1.18.32
adventure=4.17.0
rpc_sdk=1.0
discord_formatter=2.0.0
cloth_config=9.1.104
# Mod Dependencies
ftb_ranks=1904.1.1-build.82

View File

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

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

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

View File

@@ -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"
}

View File

@@ -11,6 +11,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}"
@@ -124,9 +125,11 @@ publisher {
modrinthDepends {
required("fabric-api")
optional("cloth-config", "modmenu")
}
curseDepends {
required("fabric-api")
optional("cloth-config", "modmenu")
}
}

View File

@@ -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<String, ConfigScreenFactory<?>> 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));
}
});

View File

@@ -8,6 +8,8 @@ dependencies {
stupidRemapArch("dev.ftb.mods:ftb-essentials-forge:${ftb_essentials}")
stupidRemapArch("dev.ftb.mods:ftb-ranks-forge:${ftb_ranks}")
modImplementation("me.shedaniel.cloth:cloth-config-forge:${cloth_config}")
// Do not edit or remove
implementation project(":Common")
}
@@ -116,4 +118,12 @@ publisher {
setArtifact(remapJar)
setCurseEnvironment("both")
setIsManualRelease(true)
curseDepends {
optional("cloth-config")
}
modrinthDepends {
optional("cloth-config")
}
}

View File

@@ -1,10 +1,12 @@
package com.hypherionmc.craterlib.mixin;
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.ModuleConfig;
import com.hypherionmc.craterlib.core.config.annotations.ClothScreen;
import com.hypherionmc.craterlib.core.config.annotations.NoConfigScreen;
import com.hypherionmc.craterlib.core.platform.ModloaderEnvironment;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import net.minecraftforge.client.ConfigScreenHandler;
@@ -30,9 +32,16 @@ public class ConfigScreenHandlerMixin {
@Inject(at = @At("RETURN"), method = "getScreenFactoryFor", cancellable = true, remap = false)
private static void injectConfigScreen(IModInfo selectedMod, CallbackInfoReturnable<Optional<BiFunction<Minecraft, Screen, Screen>>> cir) {
ConfigController.getWatchedConfigs().forEach((conf, watcher) -> {
if (!conf.getClass().isAnnotationPresent(NoConfigScreen.class)) {
AbstractConfig config = watcher.getLeft();
if (config.getModId().equals(selectedMod.getModId())) {
AbstractConfig config = watcher.getLeft();
if (config.getClass().isAnnotationPresent(NoConfigScreen.class))
return;
if (config.getModId().equals(selectedMod.getModId())) {
if (watcher.getLeft().getClass().isAnnotationPresent(ClothScreen.class) && ModloaderEnvironment.INSTANCE.isModLoaded("cloth_config")) {
cir.setReturnValue(
Optional.of((minecraft, screen) -> ClothConfigScreenBuilder.buildConfigScreen(config, screen))
);
} else {
cir.setReturnValue(
Optional.of((minecraft, screen) -> new CraterConfigScreen(config, screen))
);
@@ -40,5 +49,4 @@ public class ConfigScreenHandlerMixin {
}
});
}
}

View File

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

View File

@@ -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
@@ -26,6 +26,7 @@ lombok=1.18.32
adventure=4.17.0
rpc_sdk=1.0
discord_formatter=2.0.0
cloth_config=12.0.137
# Mod Dependencies
ftb_ranks=2001.1.3

View File

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

View File

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

View File

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

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.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);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More