diff --git a/.github/scripts/publish-screenshots.sh b/.github/scripts/publish-screenshots.sh index cf77ee5..26e7afb 100755 --- a/.github/scripts/publish-screenshots.sh +++ b/.github/scripts/publish-screenshots.sh @@ -5,7 +5,8 @@ # URLs) in a PR comment and in the job summary. # # Leaves the normalized screenshots in screenshots-publish/ for the comment -# step: /mc--survival.png and /mc--title.png. +# step: /mc--survival.png, /mc--title.png, +# /mc--mods.png and /mc--settings.png. # # Required env: GITHUB_TOKEN, GITHUB_REPOSITORY, GITHUB_RUN_ID, GITHUB_SHA set -euo pipefail @@ -34,7 +35,7 @@ mkdir -p "$PUBLISH" for dir in shots/*/; do mc="$(basename "$dir")" - for kind in survival-world title-screen; do + for kind in survival-world title-screen mods-menu settings; do src="$(find "$dir" -name "*betterhud-$kind.png" -print -quit)" if [ -n "$src" ]; then cp "$src" "$PUBLISH/mc-$mc-${kind%%-*}.png" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 20256d5..d447fb8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -114,6 +114,9 @@ jobs: - name: check survival world screenshot was taken run: test -n "$(find launchtest/run/screenshots -name '*betterhud-survival-world.png' -print -quit 2>/dev/null)" + - name: check settings screen screenshot was taken + run: test -n "$(find launchtest/run/screenshots -name '*betterhud-settings.png' -print -quit 2>/dev/null)" + - name: upload screenshots # Consumed by the screenshot-report job below. if: always() @@ -155,13 +158,14 @@ jobs: const dir = `screenshots-publish/${context.sha}`; const base = `https://raw.githubusercontent.com/${context.repo.owner}/${context.repo.repo}/ci-screenshots/${context.sha}`; - const cells = versions.map(v => { - const file = `mc-${v}-survival.png`; - const ok = fs.existsSync(`${dir}/${file}`); - return ok - ? `${v}
BetterHUD on Minecraft ${v}` - : `${v}
❌ no screenshot`; - }); + const shot = (v, kind, what) => { + const file = `mc-${v}-${kind}.png`; + return fs.existsSync(`${dir}/${file}`) + ? `${what} on Minecraft ${v}` + : `❌ no ${kind} screenshot`; + }; + const cells = versions.map(v => + `${v}
${shot(v, 'survival', 'BetterHUD')}
${shot(v, 'settings', 'BetterHUD settings')}`); let rows = ''; for (let i = 0; i < cells.length; i += 4) { rows += `${cells.slice(i, i + 4).join('')}\n`; @@ -170,7 +174,7 @@ jobs: const marker = ''; const body = [ marker, - `The game launched and entered a survival singleplayer world with the HUD active, on every supported Minecraft version, for \`${context.sha.slice(0, 7)}\`. A ❌ means that version's launch test did not produce a screenshot — check its job. Title-screen screenshots and game logs are in the run artifacts.`, + `The game launched, entered a survival singleplayer world with the HUD active, and opened the BetterHUD settings through Mod Menu, on every supported Minecraft version, for \`${context.sha.slice(0, 7)}\`. Each cell shows the survival world and the settings screen. A ❌ means that version's launch test did not produce that screenshot — check its job. Title-screen and mod-list screenshots and game logs are in the run artifacts.`, '', `\n${rows}
`, ].join('\n'); diff --git a/README.md b/README.md index 4fa98a4..73a10c9 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,9 @@ Requires [Cloth Config](https://modrinth.com/mod/cloth-config), [Mod Menu](https ## Configuration (requires Mod Menu) ![General Settings](https://cdn.modrinth.com/data/cached_images/9c3ed350ef0c2206a7439d1e1c15b708aa22a332.png) ![Mod Settings](https://cdn.modrinth.com/data/cached_images/f89e15c258a5a24cfda8fe9398406a7ce16edbcc.png) + ## Installation 1. Ensure you have Fabric Mod Loader installed. 2. Download the BetterHUD mod file. -3. Place the downloaded file in your Minecraft `mods` folder. +3. Place the downloaded file in your Minecraft `mods` folder. \ No newline at end of file diff --git a/launchtest/build.gradle b/launchtest/build.gradle index 49d52e8..034d4be 100644 --- a/launchtest/build.gradle +++ b/launchtest/build.gradle @@ -93,12 +93,16 @@ dependencies { mappings loom.officialMojangMappings() modImplementation "net.fabricmc:fabric-loader:${loaderVersion}" modImplementation "net.fabricmc.fabric-api:fabric-api:${entry.fabricApi}" + // The test driver opens the BetterHUD settings through Mod Menu's mod + // list; compile against the exact Mod Menu launched for this version. + modCompileOnly("com.terraformersmc:modmenu:${entry.modmenu}") { transitive = false } if (entry.clientGametest) { modImplementation fabricApi.module('fabric-client-gametest-api-v1', entry.fabricApi) } } else { implementation "net.fabricmc:fabric-loader:${loaderVersion}" implementation "net.fabricmc.fabric-api:fabric-api:${entry.fabricApi}" + compileOnly("com.terraformersmc:modmenu:${entry.modmenu}") { transitive = false } implementation fabricApi.module('fabric-client-gametest-api-v1', entry.fabricApi) } diff --git a/launchtest/src/fallback/java/dsns/betterhud/gametest/LaunchTestClient.java b/launchtest/src/fallback/java/dsns/betterhud/gametest/LaunchTestClient.java index cb21937..9c670bb 100644 --- a/launchtest/src/fallback/java/dsns/betterhud/gametest/LaunchTestClient.java +++ b/launchtest/src/fallback/java/dsns/betterhud/gametest/LaunchTestClient.java @@ -1,9 +1,13 @@ package dsns.betterhud.gametest; +import com.terraformersmc.modmenu.ModMenu; +import com.terraformersmc.modmenu.gui.ModsScreen; + import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.minecraft.client.Minecraft; import net.minecraft.client.Screenshot; +import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.TitleScreen; /** @@ -13,17 +17,23 @@ *

When the run is started with {@code --quickPlaySingleplayer ci-world} and * {@code -Dbetterhud.launchtest.expectWorld=true} (see the launchtest Gradle * project), the game joins the pre-generated survival world; once it has ticked - * long enough for chunks to render with the HUD active, this mod screenshots it - * and schedules a clean shutdown, making the client exit with code 0. Without a - * pre-generated world it screenshots the title screen and shuts down there - * instead. A crash anywhere on the way fails the run. + * long enough for chunks to render with the HUD active, this mod screenshots it, + * then opens the BetterHUD settings the way a player would (through Mod Menu's + * mod list), screenshots those screens too, and schedules a clean shutdown, + * making the client exit with code 0. Without a pre-generated world it + * screenshots the title screen and shuts down there instead. A crash anywhere + * on the way fails the run. */ public class LaunchTestClient implements ClientModInitializer { private static final boolean EXPECT_WORLD = Boolean.getBoolean("betterhud.launchtest.expectWorld"); // 200 ticks (10s) gives the software renderer on CI time to draw chunks. private static final int WORLD_SCREENSHOT_TICK = 200; - private static final int WORLD_STOP_TICK = 240; + private static final int MODS_SCREEN_TICK = 220; + private static final int MODS_SCREENSHOT_TICK = 240; + private static final int SETTINGS_SCREEN_TICK = 260; + private static final int SETTINGS_SCREENSHOT_TICK = 280; + private static final int WORLD_STOP_TICK = 300; private static final int TITLE_SCREENSHOT_TICK = 40; private static final int TITLE_STOP_TICK = 80; // 2400 ticks (2min) is plenty to join the pre-generated flat world; bail @@ -48,6 +58,14 @@ public void onInitializeClient() { worldTicks++; if (worldTicks == WORLD_SCREENSHOT_TICK) { takeScreenshot(client, "betterhud-survival-world"); + } else if (worldTicks == MODS_SCREEN_TICK) { + client.setScreen(new ModsScreen(client.screen)); + } else if (worldTicks == MODS_SCREENSHOT_TICK) { + takeScreenshot(client, "betterhud-mods-menu"); + } else if (worldTicks == SETTINGS_SCREEN_TICK) { + openBetterHudSettings(client); + } else if (worldTicks == SETTINGS_SCREENSHOT_TICK) { + takeScreenshot(client, "betterhud-settings"); } else if (worldTicks == WORLD_STOP_TICK) { client.stop(); } @@ -62,6 +80,15 @@ public void onInitializeClient() { }); } + private static void openBetterHudSettings(Minecraft client) { + Screen settings = ModMenu.getConfigScreen("betterhud", client.screen); + if (settings == null) { + System.err.println("betterhud launch test: Mod Menu has no config screen registered for betterhud"); + System.exit(1); + } + client.setScreen(settings); + } + private static void takeScreenshot(Minecraft client, String name) { Screenshot.grab(client.gameDirectory, name + ".png", client.getMainRenderTarget(), message -> {}); } diff --git a/launchtest/src/fallback/resources/fabric.mod.json b/launchtest/src/fallback/resources/fabric.mod.json index ec2bc93..15aaddb 100644 --- a/launchtest/src/fallback/resources/fabric.mod.json +++ b/launchtest/src/fallback/resources/fabric.mod.json @@ -11,6 +11,7 @@ }, "depends": { "betterhud": "*", - "fabric-lifecycle-events-v1": "*" + "fabric-lifecycle-events-v1": "*", + "modmenu": "*" } } diff --git a/launchtest/src/gametest-legacy/java/dsns/betterhud/gametest/BetterHudClientGameTest.java b/launchtest/src/gametest-legacy/java/dsns/betterhud/gametest/BetterHudClientGameTest.java index 071f168..21cca71 100644 --- a/launchtest/src/gametest-legacy/java/dsns/betterhud/gametest/BetterHudClientGameTest.java +++ b/launchtest/src/gametest-legacy/java/dsns/betterhud/gametest/BetterHudClientGameTest.java @@ -10,9 +10,13 @@ import java.nio.file.Files; import java.nio.file.Path; +import com.terraformersmc.modmenu.ModMenu; +import com.terraformersmc.modmenu.gui.ModsScreen; + import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest; import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext; import net.fabricmc.fabric.api.client.gametest.v1.context.TestSingleplayerContext; +import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState; @SuppressWarnings("UnstableApiUsage") @@ -30,6 +34,22 @@ public void runTest(ClientGameTestContext context) { context.waitTicks(40); assertScreenshotSaved(context.takeScreenshot("betterhud-survival-world")); } + + // Back on the title screen: open the BetterHUD settings the way a + // player would, through Mod Menu's mod list. + context.runOnClient(client -> client.setScreen(new ModsScreen(client.screen))); + context.waitTicks(10); + assertScreenshotSaved(context.takeScreenshot("betterhud-mods-menu")); + + context.runOnClient(client -> { + Screen settings = ModMenu.getConfigScreen("betterhud", client.screen); + if (settings == null) { + throw new AssertionError("Mod Menu has no config screen registered for betterhud"); + } + client.setScreen(settings); + }); + context.waitTicks(10); + assertScreenshotSaved(context.takeScreenshot("betterhud-settings")); } private static void assertScreenshotSaved(Path screenshot) { diff --git a/src/gametest/java/dsns/betterhud/gametest/BetterHudClientGameTest.java b/src/gametest/java/dsns/betterhud/gametest/BetterHudClientGameTest.java index 95c29ac..0781b76 100644 --- a/src/gametest/java/dsns/betterhud/gametest/BetterHudClientGameTest.java +++ b/src/gametest/java/dsns/betterhud/gametest/BetterHudClientGameTest.java @@ -5,9 +5,13 @@ import java.nio.file.Files; import java.nio.file.Path; +import com.terraformersmc.modmenu.ModMenu; +import com.terraformersmc.modmenu.gui.ModsScreen; + import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest; import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext; import net.fabricmc.fabric.api.client.gametest.v1.context.TestSingleplayerContext; +import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState; @SuppressWarnings("UnstableApiUsage") @@ -25,6 +29,22 @@ public void runTest(ClientGameTestContext context) { context.waitTicks(40); assertScreenshotSaved(context.takeScreenshot("betterhud-survival-world")); } + + // Back on the title screen: open the BetterHUD settings the way a + // player would, through Mod Menu's mod list. + context.runOnClient(client -> client.setScreen(new ModsScreen(client.screen))); + context.waitTicks(10); + assertScreenshotSaved(context.takeScreenshot("betterhud-mods-menu")); + + context.runOnClient(client -> { + Screen settings = ModMenu.getConfigScreen("betterhud", client.screen); + if (settings == null) { + throw new AssertionError("Mod Menu has no config screen registered for betterhud"); + } + client.setScreen(settings); + }); + context.waitTicks(10); + assertScreenshotSaved(context.takeScreenshot("betterhud-settings")); } private static void assertScreenshotSaved(Path screenshot) { diff --git a/src/gametest/resources/fabric.mod.json b/src/gametest/resources/fabric.mod.json index e432019..b5c39d0 100644 --- a/src/gametest/resources/fabric.mod.json +++ b/src/gametest/resources/fabric.mod.json @@ -11,6 +11,7 @@ }, "depends": { "betterhud": "*", - "fabric-client-gametest-api-v1": "*" + "fabric-client-gametest-api-v1": "*", + "modmenu": "*" } } diff --git a/src/main/java/dsns/betterhud/ModMenu.java b/src/main/java/dsns/betterhud/ModMenu.java index 70400af..787e4ec 100644 --- a/src/main/java/dsns/betterhud/ModMenu.java +++ b/src/main/java/dsns/betterhud/ModMenu.java @@ -3,17 +3,44 @@ import com.terraformersmc.modmenu.api.ConfigScreenFactory; import com.terraformersmc.modmenu.api.ModMenuApi; import dsns.betterhud.util.BaseMod; +import dsns.betterhud.util.ModSettings; import dsns.betterhud.util.Setting; import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import me.shedaniel.clothconfig2.api.AbstractConfigListEntry; import me.shedaniel.clothconfig2.api.ConfigBuilder; import me.shedaniel.clothconfig2.api.ConfigCategory; import me.shedaniel.clothconfig2.api.ConfigEntryBuilder; +import me.shedaniel.clothconfig2.api.Requirement; +import me.shedaniel.clothconfig2.gui.entries.BooleanListEntry; +import me.shedaniel.clothconfig2.impl.builders.SubCategoryBuilder; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.network.chat.Component; public class ModMenu implements ModMenuApi { + private static final Map ORIENTATION_LABELS; + private static final List STANDARD_KEYS = Arrays.asList( + "Enabled", + "Orientation", + "Custom Position", + "Custom X", + "Custom Y", + "Scale", + "Text Color", + "Background Color" + ); + + static { + ORIENTATION_LABELS = new LinkedHashMap<>(); + ORIENTATION_LABELS.put("top-left", "Top Left"); + ORIENTATION_LABELS.put("top-right", "Top Right"); + ORIENTATION_LABELS.put("bottom-left", "Bottom Left"); + ORIENTATION_LABELS.put("bottom-right", "Bottom Right"); + } + @Override public ConfigScreenFactory getModConfigScreenFactory() { if (!FabricLoader.getInstance().isModLoaded("cloth-config2")) { @@ -24,7 +51,6 @@ public ConfigScreenFactory getModConfigScreenFactory() { .setParentScreen(parent) .setTitle(Component.literal("BetterHUD Settings")); - // same as builder.setSavingRunnable(() -> Config.serialize()); builder.setSavingRunnable(Config::serialize); ConfigEntryBuilder entryBuilder = builder.entryBuilder(); @@ -33,97 +59,341 @@ public ConfigScreenFactory getModConfigScreenFactory() { ConfigCategory category = builder.getOrCreateCategory( Component.literal(mod.getModID()) ); - - for (Map.Entry entry : mod - .getModSettings() - .getSettings() - .entrySet()) { - String key = entry.getKey(); - Setting setting = entry.getValue(); - - if (setting.getType().equals("boolean")) { - category.addEntry( - entryBuilder - .startBooleanToggle( - Component.literal(key), - setting.getBooleanValue() - ) - .setDefaultValue( - Boolean.valueOf(setting.getDefaultValue()) - ) - .setSaveConsumer(value -> - setting.setValue(String.valueOf(value)) - ) - .build() - ); - } else if (setting.getType().equals("string")) { - category.addEntry( - entryBuilder - .startStringDropdownMenu( - Component.literal(key), - setting.getStringValue() - ) - .setSelections( - Arrays.asList(setting.getPossibleValues()) - ) - .setDefaultValue(setting.getDefaultValue()) - .setSaveConsumer(value -> - setting.setValue(value) - ) - .build() - ); - } else if (setting.getType().equals("integer")) { - category.addEntry( - entryBuilder - .startIntField( - Component.literal(key), - setting.getIntValue() - ) - .setDefaultValue( - Integer.parseInt(setting.getDefaultValue()) - ) - .setSaveConsumer(value -> - setting.setValue(String.valueOf(value)) - ) - .build() - ); - } else if (setting.getType().equals("double")) { - category.addEntry( - entryBuilder - .startDoubleField( - Component.literal(key), - setting.getDoubleValue() - ) - .setDefaultValue( - Double.parseDouble( - setting.getDefaultValue() - ) - ) - .setSaveConsumer(value -> - setting.setValue(String.valueOf(value)) - ) - .build() - ); - } else if (setting.getType().equals("color")) { - category.addEntry( - entryBuilder - .startAlphaColorField( - Component.literal(key), - setting.getColorValue() - ) - .setDefaultValue( - Integer.parseInt(setting.getDefaultValue()) - ) - .setSaveConsumer(value -> - setting.setValue(String.valueOf(value)) - ) - .build() - ); - } - } + buildModCategory(category, entryBuilder, mod); } return builder.build(); }; } + + private void buildModCategory( + ConfigCategory category, + ConfigEntryBuilder entryBuilder, + BaseMod mod + ) { + ModSettings settings = mod.getModSettings(); + String name = mod.getModID(); + + Setting enabled = settings.getSetting("Enabled"); + BooleanListEntry enabledEntry = entryBuilder + .startBooleanToggle( + Component.literal("Enable " + name), + enabled.getBooleanValue() + ) + .setDefaultValue(Boolean.parseBoolean(enabled.getDefaultValue())) + .setTooltip( + Component.literal( + "Show the " + name + " module on the HUD." + ) + ) + .setYesNoTextSupplier(value -> + Component.literal(value ? "Enabled" : "Disabled") + ) + .setSaveConsumer(value -> + enabled.setValue(String.valueOf(value)) + ) + .build(); + category.addEntry(enabledEntry); + + SubCategoryBuilder optionsGroup = null; + for (Map.Entry entry : settings + .getSettings() + .entrySet()) { + if (STANDARD_KEYS.contains(entry.getKey())) { + continue; + } + if (optionsGroup == null) { + optionsGroup = entryBuilder + .startSubCategory(Component.literal("Options")) + .setExpanded(true) + .setTooltip( + Component.literal( + "Settings specific to the " + name + " module." + ) + ); + } + AbstractConfigListEntry optionEntry = buildGenericEntry( + entryBuilder, + entry.getKey(), + entry.getValue() + ); + if (optionEntry != null) { + optionsGroup.add(optionEntry); + } + } + if (optionsGroup != null) { + category.addEntry(optionsGroup.build()); + } + + Setting customPosition = settings.getSetting("Custom Position"); + BooleanListEntry customPositionEntry = entryBuilder + .startBooleanToggle( + Component.literal("Use Custom Position"), + customPosition.getBooleanValue() + ) + .setDefaultValue( + Boolean.parseBoolean(customPosition.getDefaultValue()) + ) + .setTooltip( + Component.literal( + "Override the screen anchor and place this module at an exact location." + ) + ) + .setSaveConsumer(value -> + customPosition.setValue(String.valueOf(value)) + ) + .build(); + + SubCategoryBuilder positionGroup = entryBuilder + .startSubCategory(Component.literal("Position")) + .setExpanded(true) + .setTooltip( + Component.literal( + "Where the module is drawn on the screen." + ) + ); + + Setting orientation = settings.getSetting("Orientation"); + positionGroup.add( + entryBuilder + .startSelector( + Component.literal("Anchor"), + orientation.getPossibleValues(), + orientation.getStringValue() + ) + .setDefaultValue(orientation.getDefaultValue()) + .setNameProvider(value -> + Component.literal(prettyOrientation(value)) + ) + .setTooltip( + Component.literal( + "Which corner of the screen the module is anchored to." + ) + ) + .setRequirement(Requirement.isFalse(customPositionEntry)) + .setSaveConsumer(value -> orientation.setValue(value)) + .build() + ); + + positionGroup.add(customPositionEntry); + + Setting customX = settings.getSetting("Custom X"); + positionGroup.add( + entryBuilder + .startIntSlider( + Component.literal("Custom X"), + customX.getIntValue(), + parseBound(customX, 0, 0), + parseBound(customX, 1, 100) + ) + .setDefaultValue(Integer.parseInt(customX.getDefaultValue())) + .setTextGetter(value -> Component.literal(value + "%")) + .setTooltip( + Component.literal( + "Horizontal position as a percentage of screen width (0% = left edge, 100% = right edge)." + ) + ) + .setRequirement(Requirement.isTrue(customPositionEntry)) + .setSaveConsumer(value -> + customX.setValue(String.valueOf(value)) + ) + .build() + ); + + Setting customY = settings.getSetting("Custom Y"); + positionGroup.add( + entryBuilder + .startIntSlider( + Component.literal("Custom Y"), + customY.getIntValue(), + parseBound(customY, 0, 0), + parseBound(customY, 1, 100) + ) + .setDefaultValue(Integer.parseInt(customY.getDefaultValue())) + .setTextGetter(value -> Component.literal(value + "%")) + .setTooltip( + Component.literal( + "Vertical position as a percentage of screen height (0% = top edge, 100% = bottom edge)." + ) + ) + .setRequirement(Requirement.isTrue(customPositionEntry)) + .setSaveConsumer(value -> + customY.setValue(String.valueOf(value)) + ) + .build() + ); + + category.addEntry(positionGroup.build()); + + SubCategoryBuilder appearanceGroup = entryBuilder + .startSubCategory(Component.literal("Appearance")) + .setExpanded(false) + .setTooltip( + Component.literal( + "Size and colors used to draw the module." + ) + ); + + Setting scale = settings.getSetting("Scale"); + appearanceGroup.add( + entryBuilder + .startDoubleField( + Component.literal("Scale"), + scale.getDoubleValue() + ) + .setDefaultValue(Double.parseDouble(scale.getDefaultValue())) + .setMin(parseDoubleBound(scale, 0, 0.1)) + .setMax(parseDoubleBound(scale, 1, 10.0)) + .setTooltip( + Component.literal( + "Size multiplier applied to the module (1.0 = default size)." + ) + ) + .setSaveConsumer(value -> + scale.setValue(String.valueOf(value)) + ) + .build() + ); + + Setting textColor = settings.getSetting("Text Color"); + appearanceGroup.add( + entryBuilder + .startAlphaColorField( + Component.literal("Text Color"), + textColor.getColorValue() + ) + .setDefaultValue( + Integer.parseInt(textColor.getDefaultValue()) + ) + .setTooltip( + Component.literal( + "Color of the displayed text (with alpha)." + ) + ) + .setSaveConsumer(value -> + textColor.setValue(String.valueOf(value)) + ) + .build() + ); + + Setting backgroundColor = settings.getSetting("Background Color"); + appearanceGroup.add( + entryBuilder + .startAlphaColorField( + Component.literal("Background Color"), + backgroundColor.getColorValue() + ) + .setDefaultValue( + Integer.parseInt(backgroundColor.getDefaultValue()) + ) + .setTooltip( + Component.literal( + "Color of the rectangle drawn behind the text. Set alpha to 0 for no background." + ) + ) + .setSaveConsumer(value -> + backgroundColor.setValue(String.valueOf(value)) + ) + .build() + ); + + category.addEntry(appearanceGroup.build()); + } + + private AbstractConfigListEntry buildGenericEntry( + ConfigEntryBuilder entryBuilder, + String key, + Setting setting + ) { + Component label = Component.literal(key); + switch (setting.getType()) { + case "boolean": + return entryBuilder + .startBooleanToggle(label, setting.getBooleanValue()) + .setDefaultValue( + Boolean.parseBoolean(setting.getDefaultValue()) + ) + .setSaveConsumer(value -> + setting.setValue(String.valueOf(value)) + ) + .build(); + case "string": + return entryBuilder + .startSelector( + label, + setting.getPossibleValues(), + setting.getStringValue() + ) + .setDefaultValue(setting.getDefaultValue()) + .setSaveConsumer(value -> setting.setValue(value)) + .build(); + case "integer": + return entryBuilder + .startIntField(label, setting.getIntValue()) + .setMin(parseBound(setting, 0, Integer.MIN_VALUE)) + .setMax(parseBound(setting, 1, Integer.MAX_VALUE)) + .setDefaultValue( + Integer.parseInt(setting.getDefaultValue()) + ) + .setSaveConsumer(value -> + setting.setValue(String.valueOf(value)) + ) + .build(); + case "double": + return entryBuilder + .startDoubleField(label, setting.getDoubleValue()) + .setDefaultValue( + Double.parseDouble(setting.getDefaultValue()) + ) + .setSaveConsumer(value -> + setting.setValue(String.valueOf(value)) + ) + .build(); + case "color": + return entryBuilder + .startAlphaColorField(label, setting.getColorValue()) + .setDefaultValue( + Integer.parseInt(setting.getDefaultValue()) + ) + .setSaveConsumer(value -> + setting.setValue(String.valueOf(value)) + ) + .build(); + default: + return null; + } + } + + private static String prettyOrientation(String raw) { + String pretty = ORIENTATION_LABELS.get(raw); + return pretty != null ? pretty : raw; + } + + private static int parseBound(Setting setting, int index, int fallback) { + String[] possible = setting.getPossibleValues(); + if (possible == null || possible.length <= index) { + return fallback; + } + try { + return Integer.parseInt(possible[index]); + } catch (NumberFormatException e) { + return fallback; + } + } + + private static double parseDoubleBound( + Setting setting, + int index, + double fallback + ) { + String[] possible = setting.getPossibleValues(); + if (possible == null || possible.length <= index) { + return fallback; + } + try { + return Double.parseDouble(possible[index]); + } catch (NumberFormatException e) { + return fallback; + } + } }