diff --git a/Common/src/main/java/me/hypherionmc/craterlib/client/gui/config/CraterConfigScreen.java b/Common/src/main/java/me/hypherionmc/craterlib/client/gui/config/CraterConfigScreen.java new file mode 100644 index 0000000..10e4048 --- /dev/null +++ b/Common/src/main/java/me/hypherionmc/craterlib/client/gui/config/CraterConfigScreen.java @@ -0,0 +1,391 @@ +package me.hypherionmc.craterlib.client.gui.config; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.*; +import com.mojang.math.Matrix4f; +import me.hypherionmc.craterlib.CraterConstants; +import me.hypherionmc.craterlib.client.gui.config.widgets.*; +import me.hypherionmc.craterlib.common.config.ModuleConfig; +import me.hypherionmc.craterlib.common.config.annotations.SubConfig; +import me.hypherionmc.craterlib.common.config.annotations.Tooltip; +import me.hypherionmc.nightconfig.core.conversion.SpecComment; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.GuiComponent; +import net.minecraft.client.gui.screens.ConfirmScreen; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; + +import javax.annotation.Nullable; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * @author HypherionSA + * @date 03/07/2022 + */ +public class CraterConfigScreen extends Screen { + public static final float SCROLLBAR_BOTTOM_COLOR = .5f; + public static final float SCROLLBAR_TOP_COLOR = .67f; + private static final int TOP = 26; + private static final int BOTTOM = 24; + private final Screen parent; + private final List> options = new ArrayList<>(); + private final ModuleConfig config; + private final Object subConfig; + public double scrollerAmount; + private boolean dragging; + + public CraterConfigScreen(ModuleConfig config, Screen parent, Object subConfig) { + super(Component.translatable("cl." + config.getClass().getSimpleName().toLowerCase() + ".title")); + this.parent = parent; + this.config = config; + this.subConfig = subConfig; + if (subConfig != null) { + setupScreenFromConfig(subConfig, subConfig.getClass()); + } else { + setupScreenFromConfig(config, config.getClass()); + } + } + + public CraterConfigScreen(ModuleConfig config, Screen parent) { + this(config, parent, null); + } + + private static Component toText(Enum val) { + return Component.translatable(val.toString()); + } + + private static Component toText(Boolean bool) { + return Component.translatable(bool.toString()); + } + + private void setupScreenFromConfig(Object object, Class clazz) { + while (clazz != Object.class) { + for (Field field : clazz.getDeclaredFields()) { + final int fieldModifiers = field.getModifiers(); + if (object == null || Modifier.isStatic(fieldModifiers) || Modifier.isTransient(fieldModifiers)) { + continue; + } + + try { + if (!field.isAccessible()) { + field.setAccessible(true); + } + Object val = field.get(object); + + /* Lang Stuff */ + String baseLangKey = "cl." + clazz.getSimpleName().toLowerCase() + "." + field.getName().toLowerCase(); + String[] tooltipLang = field.isAnnotationPresent(SpecComment.class) ? new String[]{field.getAnnotation(SpecComment.class).value()} : new String[0]; + if (field.isAnnotationPresent(Tooltip.class)) { + tooltipLang = field.getAnnotation(Tooltip.class).value(); + } + + add(Component.translatable(baseLangKey), + val, + () -> val, + (ret) -> { + try { + field.set(object, ret); + config.saveConfig(config); + } catch (IllegalAccessException e) { + CraterConstants.LOG.error("Failed to update value for field {} in config {}", field.getName(), config.getConfigName(), e); + } + }, + field.isAnnotationPresent(SubConfig.class), + tooltipLang + ); + } catch (IllegalAccessException e) { + CraterConstants.LOG.error("Failed to access value for field {} in config {}", field.getName(), config.getConfigName(), e); + } + } + clazz = clazz.getSuperclass(); + } + } + + public void add(Component text, T value, @Nullable Supplier defaultValue, Consumer savingConsumer, boolean isSubConfig, String... langKeys) { + Option option = (Option) createOption(value, isSubConfig); + option.text = text; + option.defaultValue = defaultValue; + option.savingConsumer = savingConsumer; + option.originalValue = value; + option.value = value; + option.setLangKeys(List.of(langKeys)); + options.add(option); + option.onAdd(); + } + + private Option createOption(T value, boolean isSubConfig) { + if (value instanceof Enum) { + Object[] objects = value.getClass().getEnumConstants(); + return new ToggleButton>((List) Arrays.asList(objects), CraterConfigScreen::toText); + } + if (value instanceof Boolean) { + return new ToggleButton<>(Arrays.asList(Boolean.TRUE, Boolean.FALSE), CraterConfigScreen::toText); + } + if (value instanceof String) { + return new TextConfigOption<>(Function.identity(), Function.identity()); + } + if (value instanceof Integer) { + return new TextConfigOption<>(Objects::toString, Integer::valueOf); + } + if (value instanceof Long) { + return new TextConfigOption<>(Objects::toString, Long::valueOf); + } + if (value instanceof Double) { + return new TextConfigOption<>(Objects::toString, Double::valueOf); + } + if (value instanceof Float) { + return new TextConfigOption<>(Objects::toString, Float::valueOf); + } + if (value instanceof BigInteger) { + return new TextConfigOption<>(Objects::toString, BigInteger::new); + } + if (value instanceof BigDecimal) { + return new TextConfigOption<>(Objects::toString, BigDecimal::new); + } + if (value instanceof ResourceLocation) { + return new TextConfigOption<>(Objects::toString, ResourceLocation::new); + } + if (isSubConfig) { + return new SubConfigWidget<>(config, this, value); + } + throw new IllegalArgumentException(String.valueOf(value)); + } + + @Override + protected void init() { + super.init(); + ((List) children()).addAll(options); + + int buttonWidths = Math.min(200, (width - 50 - 12) / 3); + addRenderableWidget(new InternalConfigButton(this, width / 2 - buttonWidths - 3, height - 22, buttonWidths, 20, Component.empty(), true)); + addRenderableWidget(new InternalConfigButton(this, width / 2 + 3, height - 22, buttonWidths, 20, Component.empty(), false)); + } + + @Override + public void render(PoseStack matrices, int mouseX, int mouseY, float delta) { + overlayBackground(matrices, TOP, height - BOTTOM, 32); + + renderScrollBar(); + + matrices.pushPose(); + matrices.translate(0, 0, 500.0); + overlayBackground(matrices, 0, TOP, 64); + overlayBackground(matrices, height - BOTTOM, height, 64); + renderShadow(matrices); + drawCenteredString(matrices, font, getTitle(), width / 2, 9, 0xFFFFFF); + super.render(matrices, mouseX, mouseY, delta); + matrices.popPose(); + + int y = (int) (TOP + 4 - Math.round(scrollerAmount)); + for (Option option : options) { + int height1 = option.height(); + option.render(minecraft, font, 40, y, width - 80, height1, matrices, mouseX, mouseY, delta); + renderConfigTooltip(matrices, mouseX, mouseY, 40, y, font.width(option.text), height1, option.text.getString(), option.getLangKeys().toArray(new String[0])); + y += height1; + } + } + + private void renderScrollBar() { + int listHeight = height - BOTTOM - TOP; + int totalHeight = totalHeight(); + if (totalHeight > listHeight) { + int maxScroll = Math.max(0, totalHeight - listHeight); + int height = listHeight * listHeight / totalHeight; + height = Mth.clamp(height, 32, listHeight); + height = Math.max(10, height); + int minY = Math.min(Math.max((int) scrollerAmount * (listHeight - height) / maxScroll + TOP, TOP), this.height - BOTTOM - height); + + int scrollbarPositionMaxX = width; + int scrollbarPositionMinX = scrollbarPositionMaxX - 6; + + int maxY = this.height - BOTTOM; + RenderSystem.disableTexture(); + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder buffer = tesselator.getBuilder(); + RenderSystem.setShader(GameRenderer::getPositionColorShader); + buffer.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR); + + buffer.vertex(scrollbarPositionMinX, maxY, 0.0D).color(0, 0, 0, 255).endVertex(); + buffer.vertex(scrollbarPositionMaxX, maxY, 0.0D).color(0, 0, 0, 255).endVertex(); + buffer.vertex(scrollbarPositionMaxX, TOP, 0.0D).color(0, 0, 0, 255).endVertex(); + buffer.vertex(scrollbarPositionMinX, TOP, 0.0D).color(0, 0, 0, 255).endVertex(); + + buffer.vertex(scrollbarPositionMinX, minY + height, 0.0D).color(SCROLLBAR_BOTTOM_COLOR, SCROLLBAR_BOTTOM_COLOR, SCROLLBAR_BOTTOM_COLOR, 1).endVertex(); + buffer.vertex(scrollbarPositionMaxX, minY + height, 0.0D).color(SCROLLBAR_BOTTOM_COLOR, SCROLLBAR_BOTTOM_COLOR, SCROLLBAR_BOTTOM_COLOR, 1).endVertex(); + buffer.vertex(scrollbarPositionMaxX, minY, 0.0D).color(SCROLLBAR_BOTTOM_COLOR, SCROLLBAR_BOTTOM_COLOR, SCROLLBAR_BOTTOM_COLOR, 1).endVertex(); + buffer.vertex(scrollbarPositionMinX, minY, 0.0D).color(SCROLLBAR_BOTTOM_COLOR, SCROLLBAR_BOTTOM_COLOR, SCROLLBAR_BOTTOM_COLOR, 1).endVertex(); + buffer.vertex(scrollbarPositionMinX, (minY + height - 1), 0.0D).color(SCROLLBAR_TOP_COLOR, SCROLLBAR_TOP_COLOR, SCROLLBAR_TOP_COLOR, 1).endVertex(); + buffer.vertex((scrollbarPositionMaxX - 1), (minY + height - 1), 0.0D).color(SCROLLBAR_TOP_COLOR, SCROLLBAR_TOP_COLOR, SCROLLBAR_TOP_COLOR, 1).endVertex(); + buffer.vertex((scrollbarPositionMaxX - 1), minY, 0.0D).color(SCROLLBAR_TOP_COLOR, SCROLLBAR_TOP_COLOR, SCROLLBAR_TOP_COLOR, 1).endVertex(); + buffer.vertex(scrollbarPositionMinX, minY, 0.0D).color(SCROLLBAR_TOP_COLOR, SCROLLBAR_TOP_COLOR, SCROLLBAR_TOP_COLOR, 1).endVertex(); + tesselator.end(); + RenderSystem.disableBlend(); + RenderSystem.enableTexture(); + } + } + + private void renderShadow(PoseStack matrices) { + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder buffer = tesselator.getBuilder(); + RenderSystem.enableBlend(); + RenderSystem.blendFuncSeparate(770, 771, 0, 1); + RenderSystem.disableTexture(); + RenderSystem.setShader(GameRenderer::getPositionTexColorShader); + Matrix4f matrix = matrices.last().pose(); + buffer.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX_COLOR); + buffer.vertex(matrix, 0, TOP + 4, 0.0F).uv(0, 1).color(0, 0, 0, 0).endVertex(); + buffer.vertex(matrix, width, TOP + 4, 0.0F).uv(1, 1).color(0, 0, 0, 0).endVertex(); + buffer.vertex(matrix, width, TOP, 0.0F).uv(1, 0).color(0, 0, 0, 185).endVertex(); + buffer.vertex(matrix, 0, TOP, 0.0F).uv(0, 0).color(0, 0, 0, 185).endVertex(); + buffer.vertex(matrix, 0, height - BOTTOM, 0.0F).uv(0, 1).color(0, 0, 0, 185).endVertex(); + buffer.vertex(matrix, width, height - BOTTOM, 0.0F).uv(1, 1).color(0, 0, 0, 185).endVertex(); + buffer.vertex(matrix, width, height - BOTTOM - 4, 0.0F).uv(1, 0).color(0, 0, 0, 0).endVertex(); + buffer.vertex(matrix, 0, height - BOTTOM - 4, 0.0F).uv(0, 0).color(0, 0, 0, 0).endVertex(); + tesselator.end(); + RenderSystem.enableTexture(); + RenderSystem.disableBlend(); + } + + protected void overlayBackground(PoseStack matrices, int h1, int h2, int color) { + overlayBackground(matrices.last().pose(), 0, h1, width, h2, color, color, color, 255, 255); + } + + protected void overlayBackground(Matrix4f matrix, int minX, int minY, int maxX, int maxY, int red, int green, int blue, int startAlpha, int endAlpha) { + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder buffer = tesselator.getBuilder(); + RenderSystem.setShaderTexture(0, GuiComponent.BACKGROUND_LOCATION); + RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f); + RenderSystem.setShader(GameRenderer::getPositionTexColorShader); + buffer.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX_COLOR); + buffer.vertex(matrix, minX, maxY, 0.0F).uv(minX / 32.0F, maxY / 32.0F).color(red, green, blue, endAlpha).endVertex(); + buffer.vertex(matrix, maxX, maxY, 0.0F).uv(maxX / 32.0F, maxY / 32.0F).color(red, green, blue, endAlpha).endVertex(); + buffer.vertex(matrix, maxX, minY, 0.0F).uv(maxX / 32.0F, minY / 32.0F).color(red, green, blue, startAlpha).endVertex(); + buffer.vertex(matrix, minX, minY, 0.0F).uv(minX / 32.0F, minY / 32.0F).color(red, green, blue, startAlpha).endVertex(); + tesselator.end(); + } + + public int scrollHeight() { + int totalHeight = totalHeight(); + int listHeight = height - BOTTOM - TOP; + if (totalHeight <= listHeight) { + return 0; + } + return totalHeight - listHeight; + } + + public int totalHeight() { + int i = 8; + for (Option option : options) { + i += option.height(); + } + return i; + } + + public boolean hasErrors() { + for (Option option : options) { + if (option.hasErrors) { + return true; + } + } + return false; + } + + public boolean isEdited() { + for (Option option : options) { + if (option.isEdited()) { + return true; + } + } + return false; + } + + public void save() { + for (Option option : options) { + option.save(); + option.originalValue = option.value; + } + } + + @Override + public void onClose() { + if (isEdited()) { + minecraft.setScreen(new ConfirmScreen(this::acceptConfirm, Component.translatable("t.clc.quit_config"), + Component.translatable("t.clc.quit_config_sure"), + Component.translatable("t.clc.quit_discard"), + Component.translatable("gui.cancel"))); + } else { + minecraft.setScreen(parent); + } + } + + @Override + public boolean mouseScrolled(double d, double e, double f) { + if (e >= TOP && e <= height - BOTTOM) { + scrollerAmount = Mth.clamp(scrollerAmount - f * 16.0D, 0, scrollHeight()); + return true; + } + return super.mouseScrolled(d, e, f); + } + + @Override + public boolean mouseClicked(double d, double e, int i) { + this.dragging = i == 0 && d >= width - 6 && d < width; + return super.mouseClicked(d, e, i) || dragging; + } + + @Override + public boolean mouseDragged(double d, double e, int i, double f, double g) { + if (super.mouseDragged(d, e, i, f, g)) { + return true; + } + if (i != 0 || !this.dragging) { + return false; + } + if (e < TOP) { + scrollerAmount = 0; + } else if (e > height - BOTTOM) { + scrollerAmount = scrollHeight(); + } else { + double h = Math.max(1, this.scrollHeight()); + int j = height - BOTTOM - TOP; + int k = Mth.clamp((int) ((float) (j * j) / (float) this.scrollHeight()), 32, j - 8); + double l = Math.max(1.0, h / (double) (j - k)); + scrollerAmount = Mth.clamp(scrollerAmount + g * l, 0, scrollHeight()); + } + return true; + } + + private void acceptConfirm(boolean t) { + if (!t) { + minecraft.setScreen(this); + } else { + minecraft.setScreen(parent); + } + } + + private void renderConfigTooltip(PoseStack stack, int mouseX, int mouseY, int startX, int startY, int sizeX, int sizeY, String title, String... description) { + if (mouseX > startX && mouseX < startX + sizeX) { + if (mouseY > startY && mouseY < startY + sizeY) { + List list = new ArrayList<>(); + list.add(Component.translatable(ChatFormatting.BOLD + "" + ChatFormatting.YELLOW + title)); + for (String desc : description) { + list.add(Component.translatable(desc)); + } + renderComponentTooltip(stack, list, mouseX, mouseY); + } + } + } +} diff --git a/Common/src/main/java/me/hypherionmc/craterlib/client/gui/config/widgets/AbstractConfigWidget.java b/Common/src/main/java/me/hypherionmc/craterlib/client/gui/config/widgets/AbstractConfigWidget.java new file mode 100644 index 0000000..be8a035 --- /dev/null +++ b/Common/src/main/java/me/hypherionmc/craterlib/client/gui/config/widgets/AbstractConfigWidget.java @@ -0,0 +1,26 @@ +package me.hypherionmc.craterlib.client.gui.config.widgets; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.EditBox; + +/** Copied from Cloth Config Lite + * https://github.com/shedaniel/cloth-config-lite/blob/1.17/src/main/java/me/shedaniel/clothconfiglite/impl/option/AbstractWidgetOption.java + */ +public class AbstractConfigWidget extends BaseWidget { + + public static final int buttonWidth = 200; + public static final int buttonHeight = 20; + public W widget; + + @Override + public void render(Minecraft minecraft, Font font, int x, int y, int width, int height, PoseStack matrices, int mouseX, int mouseY, float delta) { + super.render(minecraft, font, x, y, width, height, matrices, mouseX, mouseY, delta); + int i = (widget instanceof EditBox ? 1 : 0); + widget.x = x + width - 200 - resetButtonOffset + i; + widget.y = y + i + 1; + widget.render(matrices, mouseX, mouseY, delta); + } +} diff --git a/Common/src/main/java/me/hypherionmc/craterlib/client/gui/config/widgets/BaseWidget.java b/Common/src/main/java/me/hypherionmc/craterlib/client/gui/config/widgets/BaseWidget.java new file mode 100644 index 0000000..2b47dba --- /dev/null +++ b/Common/src/main/java/me/hypherionmc/craterlib/client/gui/config/widgets/BaseWidget.java @@ -0,0 +1,62 @@ +package me.hypherionmc.craterlib.client.gui.config.widgets; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.components.Button; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.TextColor; + +/** + * Copied from Cloth Config Lite + * https://github.com/shedaniel/cloth-config-lite/blob/1.17/src/main/java/me/shedaniel/clothconfiglite/impl/option/BaseOption.java + */ +public class BaseWidget extends Option { + + public static final int resetButtonOffset = 48; + private final Button resetButton = addChild(new Button(0, 0, 46, 20, Component.literal("Reset"), this::onResetPressed)); + private boolean hideReset = false; + + private boolean isSubConfig = false; + + private void onResetPressed(Button button) { + value = defaultValue.get(); + reset(); + } + + public void hideReset() { + this.hideReset = true; + } + + public boolean isSubConfig() { + return isSubConfig; + } + + public void setSubConfig(boolean subConfig) { + isSubConfig = subConfig; + } + + @Override + public void render(Minecraft minecraft, Font font, int x, int y, int width, int height, PoseStack matrices, int mouseX, int mouseY, float delta) { + MutableComponent text = Component.literal(this.text.getString()); + boolean edited = isEdited() || hasErrors; + if (edited) { + text.withStyle(ChatFormatting.ITALIC); + if (hasErrors) { + text.withStyle(style -> style.withColor(TextColor.fromRgb(16733525))); + } + } else { + text.withStyle(ChatFormatting.GRAY); + } + font.draw(matrices, text, x, y + 8, 0xFFFFFF); + resetButton.x = x + width - 46; + resetButton.y = y + 1; + resetButton.active = isNotDefault(); + if (!hideReset) { + resetButton.render(matrices, mouseX, mouseY, delta); + } + } + +} diff --git a/Common/src/main/java/me/hypherionmc/craterlib/client/gui/config/widgets/InternalConfigButton.java b/Common/src/main/java/me/hypherionmc/craterlib/client/gui/config/widgets/InternalConfigButton.java new file mode 100644 index 0000000..7bbff7b --- /dev/null +++ b/Common/src/main/java/me/hypherionmc/craterlib/client/gui/config/widgets/InternalConfigButton.java @@ -0,0 +1,47 @@ +package me.hypherionmc.craterlib.client.gui.config.widgets; + +import com.mojang.blaze3d.vertex.PoseStack; +import me.hypherionmc.craterlib.client.gui.config.CraterConfigScreen; +import net.minecraft.client.gui.components.AbstractButton; +import net.minecraft.client.gui.narration.NarratedElementType; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.network.chat.Component; + +public class InternalConfigButton extends AbstractButton { + + CraterConfigScreen screen; + boolean cancel; + + public InternalConfigButton(CraterConfigScreen screen, int i, int j, int k, int l, Component component, boolean cancel) { + super(i, j, k, l, component); + this.screen = screen; + this.cancel = cancel; + } + + @Override + public void render(PoseStack poseStack, int i, int j, float f) { + if (cancel) { + setMessage(Component.translatable(screen.isEdited() ? "t.clc.cancel_discard" : "gui.cancel")); + } else { + boolean hasErrors = screen.hasErrors(); + active = screen.isEdited() && !hasErrors; + setMessage(Component.translatable(hasErrors ? "t.clc.error" : "t.clc.save")); + } + super.render(poseStack, i, j, f); + } + + @Override + public void onPress() { + if (cancel) { + screen.onClose(); + } else { + screen.save(); + } + } + + @Override + public void updateNarration(NarrationElementOutput narrationElementOutput) { + narrationElementOutput.add(NarratedElementType.USAGE, getMessage()); + } + +} diff --git a/Common/src/main/java/me/hypherionmc/craterlib/client/gui/config/widgets/Option.java b/Common/src/main/java/me/hypherionmc/craterlib/client/gui/config/widgets/Option.java new file mode 100644 index 0000000..9fa04ee --- /dev/null +++ b/Common/src/main/java/me/hypherionmc/craterlib/client/gui/config/widgets/Option.java @@ -0,0 +1,74 @@ +package me.hypherionmc.craterlib.client.gui.config.widgets; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.components.events.AbstractContainerEventHandler; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.network.chat.Component; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Copied from Cloth Config Lite + * https://github.com/shedaniel/cloth-config-lite/blob/1.17/src/main/java/me/shedaniel/clothconfiglite/impl/option/Option.java + */ +public abstract class Option extends AbstractContainerEventHandler { + + public Component text; + @Nullable + public Supplier defaultValue; + public Consumer savingConsumer; + public T originalValue; + public T value; + public boolean hasErrors; + public List children = new ArrayList<>(); + private List langKeys = new ArrayList<>(); + + public List getLangKeys() { + return langKeys; + } + + public void setLangKeys(List langKeys) { + this.langKeys = langKeys; + } + + public abstract void render(Minecraft minecraft, Font font, int x, int y, int width, int height, PoseStack matrices, int mouseX, int mouseY, float delta); + + public int height() { + return 22; + } + + @Override + public List children() { + return children; + } + + protected R addChild(R listener) { + ((List) children).add(listener); + return listener; + } + + public void onAdd() {} + + protected void reset() { + onAdd(); + } + + public boolean isEdited() { + return !Objects.equals(originalValue, value); + } + + protected boolean isNotDefault() { + return defaultValue != null && !Objects.equals(defaultValue.get(), value); + } + + public void save() { + savingConsumer.accept(value); + } +} diff --git a/Common/src/main/java/me/hypherionmc/craterlib/client/gui/config/widgets/SubConfigWidget.java b/Common/src/main/java/me/hypherionmc/craterlib/client/gui/config/widgets/SubConfigWidget.java new file mode 100644 index 0000000..cff7602 --- /dev/null +++ b/Common/src/main/java/me/hypherionmc/craterlib/client/gui/config/widgets/SubConfigWidget.java @@ -0,0 +1,36 @@ +package me.hypherionmc.craterlib.client.gui.config.widgets; + +import com.mojang.blaze3d.vertex.PoseStack; +import me.hypherionmc.craterlib.client.gui.config.CraterConfigScreen; +import me.hypherionmc.craterlib.common.config.ModuleConfig; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; + +public class SubConfigWidget extends AbstractConfigWidget { + + private final Object subConfig; + private final ModuleConfig config; + private final Screen screen; + + public SubConfigWidget(ModuleConfig config, Screen screen, Object subConfig) { + this.config = config; + this.subConfig = subConfig; + this.screen = screen; + this.widget = addChild(new Button(0, 0, 200, buttonHeight, Component.translatable("t.clc.opensubconfig"), this::openSubConfig)); + } + + @Override + public void render(Minecraft minecraft, Font font, int x, int y, int width, int height, PoseStack matrices, int mouseX, int mouseY, float delta) { + this.text = Component.literal(subConfig.getClass().getSimpleName().toLowerCase()); + this.hideReset(); + super.render(minecraft, font, x, y, width, height, matrices, mouseX, mouseY, delta); + } + + private void openSubConfig(Button button) { + Minecraft.getInstance().setScreen(new CraterConfigScreen(config, screen, subConfig)); + } + +} diff --git a/Common/src/main/java/me/hypherionmc/craterlib/client/gui/config/widgets/TextConfigOption.java b/Common/src/main/java/me/hypherionmc/craterlib/client/gui/config/widgets/TextConfigOption.java new file mode 100644 index 0000000..ec55e04 --- /dev/null +++ b/Common/src/main/java/me/hypherionmc/craterlib/client/gui/config/widgets/TextConfigOption.java @@ -0,0 +1,45 @@ +package me.hypherionmc.craterlib.client.gui.config.widgets; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; + +import java.util.function.Function; + +/** + * Copied from Cloth Config Lite + * https://github.com/shedaniel/cloth-config-lite/blob/1.17/src/main/java/me/shedaniel/clothconfiglite/impl/option/TextFieldOption.java + */ +public class TextConfigOption extends AbstractConfigWidget { + + private final Function toString; + private final Function fromString; + + public TextConfigOption(Function toString, Function fromString) { + this.toString = toString; + this.fromString = fromString; + this.widget = addChild(new WrappedEditBox(Minecraft.getInstance().font, 0, 0, 198, 18, null)); + } + + @Override + public void onAdd() { + widget.setMaxLength(1000000); + widget.setValue(toString.apply(value)); + widget.setResponder(this::update); + } + + @Override + public void render(Minecraft minecraft, Font font, int x, int y, int width, int height, PoseStack matrices, int mouseX, int mouseY, float delta) { + widget.setTextColor(hasErrors ? 16733525 : 14737632); + super.render(minecraft, font, x, y, width, height, matrices, mouseX, mouseY, delta); + } + + private void update(String s) { + try { + this.value = fromString.apply(s); + this.hasErrors = false; + } catch (Exception e) { + this.hasErrors = true; + } + } +} diff --git a/Common/src/main/java/me/hypherionmc/craterlib/client/gui/config/widgets/ToggleButton.java b/Common/src/main/java/me/hypherionmc/craterlib/client/gui/config/widgets/ToggleButton.java new file mode 100644 index 0000000..0a594da --- /dev/null +++ b/Common/src/main/java/me/hypherionmc/craterlib/client/gui/config/widgets/ToggleButton.java @@ -0,0 +1,33 @@ +package me.hypherionmc.craterlib.client.gui.config.widgets; + +import net.minecraft.client.gui.components.Button; +import net.minecraft.network.chat.Component; + +import java.util.List; +import java.util.function.Function; + +/** + * Copied from Cloth Config Lite + * https://github.com/shedaniel/cloth-config-lite/blob/1.17/src/main/java/me/shedaniel/clothconfiglite/impl/option/ToggleOption.java + */ +public class ToggleButton extends AbstractConfigWidget { + + private final List options; + private final Function toComponent; + + public ToggleButton(List options, Function toComponent) { + this.options = options; + this.toComponent = toComponent; + this.widget = addChild(new Button(0, 0, buttonWidth, buttonHeight, Component.empty(), this::switchNext)); + } + + @Override + public void onAdd() { + widget.setMessage(toComponent.apply(value)); + } + + private void switchNext(Button button) { + value = options.get((options.indexOf(value) + 1) % options.size()); + onAdd(); + } +} diff --git a/Common/src/main/java/me/hypherionmc/craterlib/client/gui/config/widgets/WrappedEditBox.java b/Common/src/main/java/me/hypherionmc/craterlib/client/gui/config/widgets/WrappedEditBox.java new file mode 100644 index 0000000..fe3da71 --- /dev/null +++ b/Common/src/main/java/me/hypherionmc/craterlib/client/gui/config/widgets/WrappedEditBox.java @@ -0,0 +1,25 @@ +package me.hypherionmc.craterlib.client.gui.config.widgets; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.network.chat.Component; + +public class WrappedEditBox extends EditBox { + + public WrappedEditBox(Font font, int i, int j, int k, int l, Component component) { + super(font, i, j, k, l, component); + } + + @Override + public void setFocus(boolean bl) { + for (GuiEventListener child : Minecraft.getInstance().screen.children()) { + if (child instanceof TextConfigOption option) { + WrappedEditBox box = option.widget; + box.setFocused(box == this); + } + } + super.setFocus(bl); + } +} diff --git a/Common/src/main/java/me/hypherionmc/craterlib/common/config/ModuleConfig.java b/Common/src/main/java/me/hypherionmc/craterlib/common/config/ModuleConfig.java index 0657d9c..3e654f9 100644 --- a/Common/src/main/java/me/hypherionmc/craterlib/common/config/ModuleConfig.java +++ b/Common/src/main/java/me/hypherionmc/craterlib/common/config/ModuleConfig.java @@ -16,6 +16,8 @@ public class ModuleConfig { private final transient File configPath; private final transient String networkID; + private final transient String configName; + /** * Set up the config * @@ -34,14 +36,12 @@ public class ModuleConfig { File configDir = new File("config" + (subFolder.isEmpty() ? "" : File.separator + subFolder)); configPath = new File(configDir + File.separator + configName + ".toml"); networkID = modId + ":conf_" + configName.replace("-", "_"); + this.configName = configName; /* Check if the required directories exists, otherwise we create them */ if (!configDir.exists()) { configDir.mkdirs(); } - - /* Register the Config for Watching and events */ - ConfigController.register_config(this); } /** @@ -55,6 +55,9 @@ public class ModuleConfig { } else { migrateConfig(config); } + /* Register the Config for Watching and events */ + ConfigController.register_config(this); + this.configReloaded(); } /** @@ -144,4 +147,11 @@ public class ModuleConfig { } + /** + * Get the name of the Config File + * @return + */ + public String getConfigName() { + return configName; + } } diff --git a/Common/src/main/java/me/hypherionmc/craterlib/common/config/annotations/SubConfig.java b/Common/src/main/java/me/hypherionmc/craterlib/common/config/annotations/SubConfig.java new file mode 100644 index 0000000..9cb8478 --- /dev/null +++ b/Common/src/main/java/me/hypherionmc/craterlib/common/config/annotations/SubConfig.java @@ -0,0 +1,16 @@ +package me.hypherionmc.craterlib.common.config.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author HypherionSA + * @date 03/07/2022 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface SubConfig { +} + diff --git a/Common/src/main/java/me/hypherionmc/craterlib/common/config/annotations/Tooltip.java b/Common/src/main/java/me/hypherionmc/craterlib/common/config/annotations/Tooltip.java new file mode 100644 index 0000000..2757ca2 --- /dev/null +++ b/Common/src/main/java/me/hypherionmc/craterlib/common/config/annotations/Tooltip.java @@ -0,0 +1,16 @@ +package me.hypherionmc.craterlib.common.config.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author HypherionSA + * @date 03/07/2022 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Tooltip { + String[] value(); +} diff --git a/Common/src/main/resources/assets/craterlib/lang/en_us.json b/Common/src/main/resources/assets/craterlib/lang/en_us.json new file mode 100644 index 0000000..03115d9 --- /dev/null +++ b/Common/src/main/resources/assets/craterlib/lang/en_us.json @@ -0,0 +1,8 @@ +{ + "t.clc.opensubconfig": "Open Config", + "t.clc.save": "Save", + "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" +} diff --git a/Forge/src/main/java/me/hypherionmc/craterlib/client/ForgeClientHelper.java b/Forge/src/main/java/me/hypherionmc/craterlib/client/ForgeClientHelper.java index eb9ba90..7edd0cc 100644 --- a/Forge/src/main/java/me/hypherionmc/craterlib/client/ForgeClientHelper.java +++ b/Forge/src/main/java/me/hypherionmc/craterlib/client/ForgeClientHelper.java @@ -15,7 +15,7 @@ import java.util.function.Supplier; * @author HypherionSA * @date 16/06/2022 */ -class ForgeClientHelper implements LibClientHelper { +public class ForgeClientHelper implements LibClientHelper { @Override public CreativeModeTab tabBuilder(String modid, String tabid, Supplier icon, String backgroundSuf) { diff --git a/README.md b/README.md index 24244fb..b11f6c9 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ A library mod used by HypherionSA's mods. Mostly used by Hyper Lighting 2. * Built in Optifine-Compat utilities * Various utilities for Blockstates, LANG, Math and Rendering * Cross Mod-Loader Events +* Cross Mod-Loader Config Screens (Based on [Cloth Config Lite](https://github.com/shedaniel/cloth-config-lite)) * TODO: Built in Cross Mod-Loader Network system * TODO: Various GUI widgets and Utilities * TODO: Cross Mod-Loader Dynamic Lighting