From c25790902ce014363abeb0eb31b8d94905b5bfe4 Mon Sep 17 00:00:00 2001 From: Hedzer Kuijlman Date: Sat, 12 Apr 2025 23:48:47 +0200 Subject: [PATCH] Revamped Config Screen --- README.md | 9 +- .../config/client/ClientConfig.java | 4 +- .../client/BlockEntityRendererMixin.java | 7 +- .../survivalfabric/modmenu/ConfigScreen.java | 578 +++++++++++++++++- 4 files changed, 556 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 5e39e8b..a7aec3b 100644 --- a/README.md +++ b/README.md @@ -56,14 +56,19 @@ As a challenge I'm trying to make it as user-friendly as possible. - [x] Render block entities from a longer range - [x] Toggleable via GUI - [x] Configurable value - - [ ] In GUI + - [x] In GUI +- [x] Rework Mod Menu integration to be more flexible + - The following types are accepted: + - String + - Boolean + - Float + - Integer # To-do ## General - Rework config system - Store server settings in world folder for better singleplayer use -- Rework Mod Menu integration to be more flexible ## Server Side - Telekinesis diff --git a/src/client/java/wtf/hak/survivalfabric/config/client/ClientConfig.java b/src/client/java/wtf/hak/survivalfabric/config/client/ClientConfig.java index 1e4fb84..2611e7e 100644 --- a/src/client/java/wtf/hak/survivalfabric/config/client/ClientConfig.java +++ b/src/client/java/wtf/hak/survivalfabric/config/client/ClientConfig.java @@ -11,6 +11,6 @@ public class ClientConfig { public boolean renderSnowFog = false; public boolean removeDarknessEffect = true; public boolean lockTeleportHeadMovement = true; - public boolean alwaysRenderBlockEntities = true; - public int renderBlockEntitiesRange = 512; + public boolean manipulateBlockEntityDistance = true; + public int blockEntityRange = 512; } diff --git a/src/client/java/wtf/hak/survivalfabric/mixin/client/BlockEntityRendererMixin.java b/src/client/java/wtf/hak/survivalfabric/mixin/client/BlockEntityRendererMixin.java index e238165..7654cbd 100644 --- a/src/client/java/wtf/hak/survivalfabric/mixin/client/BlockEntityRendererMixin.java +++ b/src/client/java/wtf/hak/survivalfabric/mixin/client/BlockEntityRendererMixin.java @@ -1,14 +1,11 @@ package wtf.hak.survivalfabric.mixin.client; import net.minecraft.block.entity.BlockEntity; -import net.minecraft.client.render.Camera; -import net.minecraft.client.render.block.entity.BlockEntityRenderDispatcher; import net.minecraft.client.render.block.entity.BlockEntityRenderer; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -import wtf.hak.survivalfabric.config.ConfigManager; import wtf.hak.survivalfabric.config.client.ClientConfigManager; @Mixin(BlockEntityRenderer.class) @@ -16,7 +13,7 @@ public interface BlockEntityRendererMixin { @Inject(method = "getRenderDistance", at = @At("HEAD"), cancellable = true) private void getRenderDistance(CallbackInfoReturnable cir) { - if(ClientConfigManager.getConfig().alwaysRenderBlockEntities) - cir.setReturnValue(ClientConfigManager.getConfig().renderBlockEntitiesRange); + if(ClientConfigManager.getConfig().manipulateBlockEntityDistance) + cir.setReturnValue(ClientConfigManager.getConfig().blockEntityRange); } } diff --git a/src/client/java/wtf/hak/survivalfabric/modmenu/ConfigScreen.java b/src/client/java/wtf/hak/survivalfabric/modmenu/ConfigScreen.java index 6b4843f..209bd99 100644 --- a/src/client/java/wtf/hak/survivalfabric/modmenu/ConfigScreen.java +++ b/src/client/java/wtf/hak/survivalfabric/modmenu/ConfigScreen.java @@ -3,15 +3,29 @@ package wtf.hak.survivalfabric.modmenu; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.TextFieldWidget; import net.minecraft.text.Text; +import net.minecraft.util.math.MathHelper; import wtf.hak.survivalfabric.config.client.ClientConfig; import wtf.hak.survivalfabric.config.client.ClientConfigManager; import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; public class ConfigScreen extends Screen { + private static final int OPTION_HEIGHT = 25; + private static final int SCROLL_BAR_WIDTH = 6; + private static final int TOP_PADDING = 40; + private static final int BOTTOM_PADDING = 35; + private static final int SIDE_PADDING = 20; private final Screen parent; + private final List> options = new ArrayList<>(); + private TextFieldWidget activeTextField = null; + private float scrollPosition = 0.0F; + private boolean scrolling = false; + private int contentHeight = 0; public ConfigScreen(Screen parent) { super(Text.literal("Survival Fabric - Client Config")); @@ -20,55 +34,272 @@ public class ConfigScreen extends Screen { @Override protected void init() { - int buttonWidth = 200; - int buttonHeight = 20; - int spacing = 5; - int startY = 40; - int i = 0; + options.clear(); + int listWidth = this.width - (SIDE_PADDING * 2); + int listHeight = this.height - TOP_PADDING - BOTTOM_PADDING; + int listLeft = SIDE_PADDING; + int listTop = TOP_PADDING; + + int index = 0; for (Field field : ClientConfig.class.getFields()) { - if (field.getType() == boolean.class) { - int y = startY + i * (buttonHeight + spacing); - try { + try { + Class type = field.getType(); + String name = formatFieldName(field.getName()); + + ConfigOption option = null; + if (type == boolean.class) { boolean value = field.getBoolean(ClientConfigManager.getConfig()); - String label = formatFieldName(field.getName()) + ": " + (value ? "ON" : "OFF"); - - ButtonWidget button = ButtonWidget.builder( - Text.literal(label), - b -> { - try { - boolean current = field.getBoolean(ClientConfigManager.getConfig()); - field.setBoolean(ClientConfigManager.getConfig(), !current); - b.setMessage(Text.literal(formatFieldName(field.getName()) + ": " + (!current ? "ON" : "OFF"))); - ClientConfigManager.save(); // Save if needed - } catch (Exception e) { - e.printStackTrace(); - } - } - ).dimensions(this.width / 2 - buttonWidth / 2, y, buttonWidth, buttonHeight).build(); - - this.addDrawableChild(button); - i++; - } catch (Exception e) { - e.printStackTrace(); + option = new BooleanConfigOption( + name, + field, + value, + listLeft, + listTop + (index * OPTION_HEIGHT) - (int) scrollPosition, + listWidth + ); + } else if (type == int.class) { + int value = field.getInt(ClientConfigManager.getConfig()); + option = new IntegerConfigOption( + name, + field, + value, + listLeft, + listTop + (index * OPTION_HEIGHT) - (int) scrollPosition, + listWidth + ); + } else if (type == float.class) { + float value = field.getFloat(ClientConfigManager.getConfig()); + option = new FloatConfigOption( + name, + field, + value, + listLeft, + listTop + (index * OPTION_HEIGHT) - (int) scrollPosition, + listWidth + ); + } else if (type == String.class) { + String value = (String) field.get(ClientConfigManager.getConfig()); + option = new StringConfigOption( + name, + field, + value, + listLeft, + listTop + (index * OPTION_HEIGHT) - (int) scrollPosition, + listWidth + ); } + + if (option != null) { + options.add(option); + index++; + } + } catch (Exception e) { + e.printStackTrace(); } } + for (ConfigOption option : options) { + if (option instanceof NumericConfigOption) { + ((NumericConfigOption) option).createTextField(this.client); + } + } + + contentHeight = options.size() * OPTION_HEIGHT; + this.addDrawableChild(ButtonWidget.builder( Text.translatable("gui.done"), button -> this.client.setScreen(parent) - ).dimensions(this.width / 2 - 75, startY + i * (buttonHeight + spacing) + 10, 150, 20).build()); + ).dimensions(this.width / 2 - 100, this.height - 27, 200, 20).build()); } @Override public void render(DrawContext context, int mouseX, int mouseY, float delta) { - context.fill(0, 0, this.width, this.height, 0xC0101010); + this.renderBackground(context, mouseX, mouseY, delta); int titleX = (this.width / 2) - (this.textRenderer.getWidth(this.title) / 2); - context.drawTextWithShadow(this.textRenderer, this.title, titleX, 20, 0xFFFFFF); + context.drawText(this.textRenderer, this.title, titleX, 15, 0xFFFFFF, true); - super.render(context, mouseX, mouseY, delta); + int listWidth = this.width - (SIDE_PADDING * 2); + int listHeight = this.height - TOP_PADDING - BOTTOM_PADDING; + int listLeft = SIDE_PADDING; + int listTop = TOP_PADDING; + + context.drawBorder(listLeft, listTop, listLeft + listWidth, listTop + listHeight, 0x00000000); + + context.enableScissor( + listLeft, + listTop, + listLeft + listWidth, + listTop + listHeight + ); + + for (int i = 0; i < options.size(); i++) { + ConfigOption option = options.get(i); + option.y = listTop + (i * OPTION_HEIGHT) - (int) scrollPosition; + + if (option instanceof NumericConfigOption) { + ((NumericConfigOption) option).updateTextFieldPosition(); + } + + if (option.y < listTop + listHeight && option.y + OPTION_HEIGHT > listTop) { + option.render(context, mouseX, mouseY, delta); + } + } + + context.disableScissor(); + + if (contentHeight > listHeight) { + int scrollBarHeight = Math.max(20, (int) ((float) listHeight / (float) contentHeight * listHeight)); + int scrollBarY = listTop + (int) ((scrollPosition / (contentHeight - listHeight)) * (listHeight - scrollBarHeight)); + + context.fill( + listLeft + listWidth + 2, + listTop, + listLeft + listWidth + 2 + SCROLL_BAR_WIDTH, + listTop + listHeight, + 0xFF404040 + ); + + context.fill( + listLeft + listWidth + 2, + scrollBarY, + listLeft + listWidth + 2 + SCROLL_BAR_WIDTH, + scrollBarY + scrollBarHeight, + scrolling ? 0xFFAAAAAA : 0xFF808080 + ); + } + + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + int listWidth = this.width - (SIDE_PADDING * 2); + int listHeight = this.height - TOP_PADDING - BOTTOM_PADDING; + int listLeft = SIDE_PADDING; + int listTop = TOP_PADDING; + + boolean handledByTextField = false; + + activeTextField = null; + + if (mouseX >= listLeft && + mouseX <= listLeft + listWidth && + mouseY >= listTop && + mouseY <= listTop + listHeight) { + + for (ConfigOption option : options) { + if (option instanceof NumericConfigOption numOption && + option.y >= listTop && + option.y + OPTION_HEIGHT <= listTop + listHeight) { + + TextFieldWidget textField = numOption.getTextField(); + + if (textField.isMouseOver(mouseX, mouseY)) { + textField.setFocused(true); + activeTextField = textField; + handledByTextField = true; + + for (ConfigOption otherOption : options) { + if (otherOption instanceof NumericConfigOption && otherOption != option) { + ((NumericConfigOption) otherOption).getTextField().setFocused(false); + } + } + + return true; + } else { + textField.setFocused(false); + } + } + } + } + + if (contentHeight > listHeight && + mouseX >= listLeft + listWidth + 2 && + mouseX <= listLeft + listWidth + 2 + SCROLL_BAR_WIDTH && + mouseY >= listTop && + mouseY <= listTop + listHeight) { + + scrolling = true; + return true; + } + + // Check if clicked on an option + if (mouseX >= listLeft && + mouseX <= listLeft + listWidth && + mouseY >= listTop && + mouseY <= listTop + listHeight) { + + for (ConfigOption option : options) { + if (option.isMouseOver(mouseX, mouseY) && option.y >= listTop && option.y + OPTION_HEIGHT <= listTop + listHeight) { + option.onClick(mouseX, mouseY); + return true; + } + } + } + + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (activeTextField != null && activeTextField.isFocused()) { + return activeTextField.keyPressed(keyCode, scanCode, modifiers); + } + + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean charTyped(char chr, int modifiers) { + if (activeTextField != null && activeTextField.isFocused()) { + return activeTextField.charTyped(chr, modifiers); + } + + return super.charTyped(chr, modifiers); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + scrolling = false; + return super.mouseReleased(mouseX, mouseY, button); + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { + int listHeight = this.height - TOP_PADDING - BOTTOM_PADDING; + int listTop = TOP_PADDING; + + if (scrolling && contentHeight > listHeight) { + float scrollAmount = (float) deltaY / (listHeight - Math.max(20, (int) ((float) listHeight / (float) contentHeight * listHeight))); + float maxScroll = contentHeight - listHeight; + + scrollPosition = MathHelper.clamp(scrollPosition + scrollAmount * maxScroll, 0.0F, maxScroll); + return true; + } + + return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) { + int listWidth = this.width - (SIDE_PADDING * 2); + int listHeight = this.height - TOP_PADDING - BOTTOM_PADDING; + int listLeft = SIDE_PADDING; + int listTop = TOP_PADDING; + + if (mouseX >= listLeft && + mouseX <= listLeft + listWidth && + mouseY >= listTop && + mouseY <= listTop + listHeight && + contentHeight > listHeight) { + + float maxScroll = contentHeight - listHeight; + scrollPosition = MathHelper.clamp(scrollPosition - (float) verticalAmount * 10, 0.0F, maxScroll); + return true; + } + + return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount); } private String formatFieldName(String rawName) { @@ -84,4 +315,285 @@ public class ConfigScreen extends Screen { return result.toString(); } -} + /** + * Base class for all config options + */ + private abstract class ConfigOption { + protected final String name; + protected final Field field; + protected final int width; + protected T value; + protected int x; + protected int y; + + public ConfigOption(String name, Field field, T initialValue, int x, int y, int width) { + this.name = name; + this.field = field; + this.value = initialValue; + this.x = x; + this.y = y; + this.width = width; + } + + public abstract void render(DrawContext context, int mouseX, int mouseY, float delta); + + public abstract void onClick(double mouseX, double mouseY); + + public boolean isMouseOver(double mouseX, double mouseY) { + return mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + OPTION_HEIGHT; + } + + protected abstract void saveValue(); + } + + /** + * Implementation for boolean config options + */ + private class BooleanConfigOption extends ConfigOption { + private static final int BUTTON_WIDTH = 40; + private static final int BUTTON_HEIGHT = 20; + + public BooleanConfigOption(String name, Field field, Boolean initialValue, int x, int y, int width) { + super(name, field, initialValue, x, y, width); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + context.drawText(textRenderer, name, x + 5, y + (OPTION_HEIGHT - 8) / 2, 0xFFFFFF, true); + + int buttonX = x + width - BUTTON_WIDTH - 5; + int buttonY = y + (OPTION_HEIGHT - BUTTON_HEIGHT) / 2; + + boolean hovered = isButtonHovered(mouseX, mouseY); + int buttonColor = hovered ? 0xFF404040 : 0xFF303030; + int buttonBorder = hovered ? 0xFFCCCCCC : 0xFF808080; + + context.fill(buttonX, buttonY, buttonX + BUTTON_WIDTH, buttonY + BUTTON_HEIGHT, buttonBorder); + context.fill(buttonX + 1, buttonY + 1, buttonX + BUTTON_WIDTH - 1, buttonY + BUTTON_HEIGHT - 1, buttonColor); + + String buttonText = value ? "true" : "false"; + int textColor = value ? 0x00be00 : 0xbe0000; + int textWidth = textRenderer.getWidth(buttonText); + context.drawText( + textRenderer, + buttonText, + buttonX + (BUTTON_WIDTH - textWidth) / 2, + buttonY + (BUTTON_HEIGHT - 8) / 2, + textColor, + true + ); + } + + public boolean isButtonHovered(double mouseX, double mouseY) { + int buttonX = x + width - BUTTON_WIDTH - 5; + int buttonY = y + (OPTION_HEIGHT - BUTTON_HEIGHT) / 2; + return mouseX >= buttonX && mouseX <= buttonX + BUTTON_WIDTH && + mouseY >= buttonY && mouseY <= buttonY + BUTTON_HEIGHT; + } + + @Override + public void onClick(double mouseX, double mouseY) { + if (isButtonHovered(mouseX, mouseY)) { + value = !value; + saveValue(); + } + } + + @Override + protected void saveValue() { + try { + // Update config and save + field.setBoolean(ClientConfigManager.getConfig(), value); + ClientConfigManager.save(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + /** + * Implementation for String config options + */ + private class StringConfigOption extends ConfigOption { + private static final int FIELD_WIDTH = 60; + private static final int FIELD_HEIGHT = 16; + protected TextFieldWidget textField; + + public StringConfigOption(String name, Field field, String initialValue, int x, int y, int width) { + super(name, field, initialValue, x, y, width); + } + + public void createTextField(net.minecraft.client.MinecraftClient client) { + int fieldX = x + width - FIELD_WIDTH - 5; + int fieldY = y + (OPTION_HEIGHT - FIELD_HEIGHT) / 2; + + textField = new TextFieldWidget( + textRenderer, + fieldX, + fieldY, + FIELD_WIDTH, + FIELD_HEIGHT, + Text.literal("") + ); + + textField.setText(value); + textField.setMaxLength(10); + textField.setChangedListener(this::onTextChanged); + } + + public TextFieldWidget getTextField() { + return textField; + } + + public void updateTextFieldPosition() { + if (textField != null) { + int fieldX = x + width - FIELD_WIDTH - 5; + int fieldY = y + (OPTION_HEIGHT - FIELD_HEIGHT) / 2; + textField.setX(fieldX); + textField.setY(fieldY); + } + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + context.drawText(textRenderer, name, x + 5, y + (OPTION_HEIGHT - 8) / 2, 0xFFFFFF, true); + + if (textField != null) { + textField.render(context, mouseX, mouseY, delta); + } + } + + @Override + public void onClick(double mouseX, double mouseY) { + } + + public void onTextChanged(String text) { + try { + value = text; + saveValue(); + } catch (NumberFormatException e) {} + } + + @Override + protected void saveValue() { + try { + field.set(ClientConfigManager.getConfig(), value); + ClientConfigManager.save(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + /** + * Base class for numeric config options (int and float) + */ + private abstract class NumericConfigOption extends ConfigOption { + private static final int FIELD_WIDTH = 60; + private static final int FIELD_HEIGHT = 16; + protected TextFieldWidget textField; + + public NumericConfigOption(String name, Field field, T initialValue, int x, int y, int width) { + super(name, field, initialValue, x, y, width); + } + + public void createTextField(net.minecraft.client.MinecraftClient client) { + int fieldX = x + width - FIELD_WIDTH - 5; + int fieldY = y + (OPTION_HEIGHT - FIELD_HEIGHT) / 2; + + textField = new TextFieldWidget( + textRenderer, + fieldX, + fieldY, + FIELD_WIDTH, + FIELD_HEIGHT, + Text.literal("") + ); + + textField.setText(value.toString()); + textField.setMaxLength(10); + textField.setChangedListener(this::onTextChanged); + } + + public TextFieldWidget getTextField() { + return textField; + } + + public void updateTextFieldPosition() { + if (textField != null) { + int fieldX = x + width - FIELD_WIDTH - 5; + int fieldY = y + (OPTION_HEIGHT - FIELD_HEIGHT) / 2; + textField.setX(fieldX); + textField.setY(fieldY); + } + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + context.drawText(textRenderer, name, x + 5, y + (OPTION_HEIGHT - 8) / 2, 0xFFFFFF, true); + + if (textField != null) { + textField.render(context, mouseX, mouseY, delta); + } + } + + @Override + public void onClick(double mouseX, double mouseY) {} + + protected abstract void onTextChanged(String text); + } + + /** + * Implementation for integer config options + */ + private class IntegerConfigOption extends NumericConfigOption { + public IntegerConfigOption(String name, Field field, Integer initialValue, int x, int y, int width) { + super(name, field, initialValue, x, y, width); + } + + @Override + protected void onTextChanged(String text) { + try { + value = Integer.parseInt(text); + saveValue(); + } catch (NumberFormatException e) {} + } + + @Override + protected void saveValue() { + try { + field.setInt(ClientConfigManager.getConfig(), value); + ClientConfigManager.save(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + /** + * Implementation for float config options + */ + private class FloatConfigOption extends NumericConfigOption { + public FloatConfigOption(String name, Field field, Float initialValue, int x, int y, int width) { + super(name, field, initialValue, x, y, width); + } + + @Override + protected void onTextChanged(String text) { + try { + value = Float.parseFloat(text); + saveValue(); + } catch (NumberFormatException e) {} + } + + @Override + protected void saveValue() { + try { + field.setFloat(ClientConfigManager.getConfig(), value); + ClientConfigManager.save(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } +} \ No newline at end of file