From fa93abbb3beaecc20e4c11646817c330b4d63ac4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 09:54:55 +0000 Subject: [PATCH 01/11] Initial plan From 0689b0bfde6fc06f35c06d7c42ced89b0048f2af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 10:05:54 +0000 Subject: [PATCH 02/11] Port Stats Ring from ChatTriggers to Fabric mod - Add ModConfig with Properties-based serialization - Add ConfigScreen using Cloth Config API - Add StatsRingRenderer porting main.js logic - Update StatsRing entry point with /ring command - Add Cloth Config dependency Co-authored-by: bubner <81782264+bubner@users.noreply.github.com> --- build.gradle | 12 +- gradle.properties | 3 +- gradlew | 0 .../me/bubner/statsring/ConfigScreen.java | 89 ++++++ .../java/me/bubner/statsring/ModConfig.java | 79 ++++++ .../java/me/bubner/statsring/StatsRing.java | 25 +- .../bubner/statsring/StatsRingRenderer.java | 264 ++++++++++++++++++ src/main/resources/fabric.mod.json | 3 +- 8 files changed, 466 insertions(+), 9 deletions(-) mode change 100644 => 100755 gradlew create mode 100644 src/main/java/me/bubner/statsring/ConfigScreen.java create mode 100644 src/main/java/me/bubner/statsring/ModConfig.java create mode 100644 src/main/java/me/bubner/statsring/StatsRingRenderer.java diff --git a/build.gradle b/build.gradle index 3157e44..fb7c61e 100644 --- a/build.gradle +++ b/build.gradle @@ -11,11 +11,7 @@ base { } repositories { - // Add repositories to retrieve artifacts from in here. - // You should only use this when depending on other mods because - // Loom adds the essential maven repositories to download Minecraft and libraries from automatically. - // See https://docs.gradle.org/current/userguide/declaring_repositories.html - // for more information about repositories. + maven { url "https://maven.shedaniel.me/" } } dependencies { @@ -26,7 +22,11 @@ dependencies { // Fabric API. This is technically optional, but you probably want it anyway. modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}" - + + // Cloth Config for settings GUI + modApi("me.shedaniel.cloth:cloth-config-fabric:${project.cloth_config_version}") { + exclude(group: "net.fabricmc.fabric-api") + } } processResources { diff --git a/gradle.properties b/gradle.properties index 9937105..e1d3d4c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,4 +17,5 @@ maven_group=me.bubner.statsring archives_base_name=statsring # Dependencies -fabric_api_version=0.138.4+1.21.10 \ No newline at end of file +fabric_api_version=0.138.4+1.21.10 +cloth_config_version=17.0.144 \ No newline at end of file diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/me/bubner/statsring/ConfigScreen.java b/src/main/java/me/bubner/statsring/ConfigScreen.java new file mode 100644 index 0000000..9cf45f2 --- /dev/null +++ b/src/main/java/me/bubner/statsring/ConfigScreen.java @@ -0,0 +1,89 @@ +package me.bubner.statsring; + +import me.shedaniel.clothconfig2.api.ConfigBuilder; +import me.shedaniel.clothconfig2.api.ConfigCategory; +import me.shedaniel.clothconfig2.api.ConfigEntryBuilder; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; + +/** + * Cloth Config screen for StatsRing settings. + * Replaces the original Vigilance config GUI from config.js. + * + * @author Lucas Bubner, 2023-2025 + */ +public class ConfigScreen { + private ConfigScreen() { + } + + /** + * Create the Cloth Config settings screen. + * + * @param parent the parent screen to return to + * @return the built config screen + */ + public static Screen create(Screen parent) { + ModConfig config = StatsRing.CONFIG; + ConfigBuilder builder = ConfigBuilder.create() + .setParentScreen(parent) + .setTitle(Component.literal("\u00A7lStats Ring: \u00A7r\u00A7cHealth \u00A7rand \u00A7bMana \u00A7rring")); + + ConfigEntryBuilder entryBuilder = builder.entryBuilder(); + + // Core category + ConfigCategory core = builder.getOrCreateCategory(Component.literal("Core")); + core.addEntry(entryBuilder.startBooleanToggle(Component.literal("Enable ring"), config.getActive()) + .setDefaultValue(true) + .setTooltip(Component.literal("Render the ring (on/off).")) + .setSaveConsumer(val -> config.properties.setProperty("active", String.valueOf(val))) + .build()); + core.addEntry(entryBuilder.startBooleanToggle(Component.literal("Show percentages"), config.getPercentage()) + .setDefaultValue(true) + .setTooltip(Component.literal("Render percentages of health/mana next to ring (on/off).")) + .setSaveConsumer(val -> config.properties.setProperty("percentage", String.valueOf(val))) + .build()); + core.addEntry(entryBuilder.startBooleanToggle(Component.literal("Show absorption"), config.getAbsorption()) + .setDefaultValue(true) + .setTooltip(Component.literal("Show over 100% health for absorption hearts (on/off).")) + .setSaveConsumer(val -> config.properties.setProperty("absorption", String.valueOf(val))) + .build()); + + // Display category + ConfigCategory display = builder.getOrCreateCategory(Component.literal("Display")); + display.addEntry(entryBuilder.startBooleanToggle(Component.literal("Show backing image"), config.getBackingImage()) + .setDefaultValue(true) + .setTooltip(Component.literal("Show the background image for the ring that surrounds the bars (on/off).")) + .setSaveConsumer(val -> config.properties.setProperty("backingImage", String.valueOf(val))) + .build()); + display.addEntry(entryBuilder.startBooleanToggle(Component.literal("Interpolate colour"), config.getInterpolateColour()) + .setDefaultValue(true) + .setTooltip(Component.literal("Linear interpolates mana and health colour depending on percentage filled (on/off).")) + .setSaveConsumer(val -> config.properties.setProperty("interpolateColour", String.valueOf(val))) + .build()); + display.addEntry(entryBuilder.startBooleanToggle(Component.literal("Interpolate bars"), config.getInterpolateBars()) + .setDefaultValue(true) + .setTooltip(Component.literal("Linear interpolates the filled progress of the bars (on/off).")) + .setSaveConsumer(val -> config.properties.setProperty("interpolateBars", String.valueOf(val))) + .build()); + + // Visual Warnings category + ConfigCategory warnings = builder.getOrCreateCategory(Component.literal("Visual Warnings")); + warnings.addEntry(entryBuilder.startIntField(Component.literal("Low HP percent alert threshold"), (int) config.getAlertLowHpPercent()) + .setDefaultValue(40) + .setMin(-1) + .setMax(100) + .setTooltip(Component.literal("Visually flashes HP when equal to or below this percentage (% from 0 to 100, -1 to disable)")) + .setSaveConsumer(val -> config.properties.setProperty("alertLowHpPercent", String.valueOf(val))) + .build()); + warnings.addEntry(entryBuilder.startIntField(Component.literal("Low Mana percent alert threshold"), (int) config.getAlertLowManaPercent()) + .setDefaultValue(20) + .setMin(-1) + .setMax(100) + .setTooltip(Component.literal("Visually flashes Mana when equal to or below this percentage (% from 0 to 100, -1 to disable)")) + .setSaveConsumer(val -> config.properties.setProperty("alertLowManaPercent", String.valueOf(val))) + .build()); + + builder.setSavingRunnable(config::save); + return builder.build(); + } +} diff --git a/src/main/java/me/bubner/statsring/ModConfig.java b/src/main/java/me/bubner/statsring/ModConfig.java new file mode 100644 index 0000000..340623d --- /dev/null +++ b/src/main/java/me/bubner/statsring/ModConfig.java @@ -0,0 +1,79 @@ +package me.bubner.statsring; + +import net.fabricmc.loader.api.FabricLoader; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Properties; + +/** + * Properties-based configuration for StatsRing. + * Serialises settings to a .properties file in the Fabric config directory. + * + * @author Lucas Bubner, 2023-2025 + */ +public class ModConfig { + private static final Path CONFIG_PATH = FabricLoader.getInstance().getConfigDir().resolve("statsring.properties"); + public final Properties properties = new Properties(); + + public void load() { + if (Files.exists(CONFIG_PATH)) { + try (var reader = Files.newBufferedReader(CONFIG_PATH)) { + properties.load(reader); + } catch (IOException e) { + StatsRing.LOGGER.error("Failed to load config", e); + } + } else { + save(); + } + } + + public void save() { + try (var writer = Files.newBufferedWriter(CONFIG_PATH)) { + properties.store(writer, "StatsRing configuration"); + } catch (IOException e) { + StatsRing.LOGGER.error("Failed to save config", e); + } + } + + public boolean getActive() { + return Boolean.parseBoolean(properties.getProperty("active", "true")); + } + + public boolean getPercentage() { + return Boolean.parseBoolean(properties.getProperty("percentage", "true")); + } + + public boolean getAbsorption() { + return Boolean.parseBoolean(properties.getProperty("absorption", "true")); + } + + public boolean getBackingImage() { + return Boolean.parseBoolean(properties.getProperty("backingImage", "true")); + } + + public boolean getInterpolateColour() { + return Boolean.parseBoolean(properties.getProperty("interpolateColour", "true")); + } + + public boolean getInterpolateBars() { + return Boolean.parseBoolean(properties.getProperty("interpolateBars", "true")); + } + + public float getAlertLowHpPercent() { + try { + return Float.parseFloat(properties.getProperty("alertLowHpPercent", "40")); + } catch (NumberFormatException e) { + return 40f; + } + } + + public float getAlertLowManaPercent() { + try { + return Float.parseFloat(properties.getProperty("alertLowManaPercent", "20")); + } catch (NumberFormatException e) { + return 20f; + } + } +} diff --git a/src/main/java/me/bubner/statsring/StatsRing.java b/src/main/java/me/bubner/statsring/StatsRing.java index 19b64f8..416a93e 100644 --- a/src/main/java/me/bubner/statsring/StatsRing.java +++ b/src/main/java/me/bubner/statsring/StatsRing.java @@ -1,16 +1,39 @@ package me.bubner.statsring; import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.minecraft.client.Minecraft; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * StatsRing - Display Health and Mana in a ring around the crosshair on Hypixel SkyBlock. + * Ported from a 1.8.9 ChatTriggers module into a 1.21.10 Fabric mod. + * + * @author Lucas Bubner, 2023-2025 + */ public class StatsRing implements ClientModInitializer { public static final String MOD_ID = "statsring"; public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + public static final ModConfig CONFIG = new ModConfig(); @Override public void onInitializeClient() { - + CONFIG.load(); + + new StatsRingRenderer(CONFIG).register(); + + // Register /ring command to open the settings GUI + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> + dispatcher.register(ClientCommandManager.literal("ring").executes(context -> { + Minecraft client = Minecraft.getInstance(); + client.execute(() -> client.setScreen(ConfigScreen.create(null))); + return 1; + })) + ); + + LOGGER.info("StatsRing initialised"); } } \ No newline at end of file diff --git a/src/main/java/me/bubner/statsring/StatsRingRenderer.java b/src/main/java/me/bubner/statsring/StatsRingRenderer.java new file mode 100644 index 0000000..91510fe --- /dev/null +++ b/src/main/java/me/bubner/statsring/StatsRingRenderer.java @@ -0,0 +1,264 @@ +package me.bubner.statsring; + +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.scores.DisplaySlot; +import net.minecraft.world.scores.Objective; +import net.minecraft.world.scores.Scoreboard; + +/** + * Port of main.js from the ChatTriggers StatsRing module. + * Handles action bar parsing for HP/Mana and renders the ring overlay on the HUD. + * + * @author Lucas Bubner, 2023-2025 + */ +public class StatsRingRenderer { + private static final ResourceLocation RING_TEXTURE = ResourceLocation.fromNamespaceAndPath("statsring", "ring-2.png"); + + private static final int HEIGHT_SCALE = 21; + private static final int RING_SIZE = 35; + private static final int BAR_WIDTH = 3; + + // Predefined colours (ARGB) + private static final int COLOR_RED = 0xFFFF0000; + private static final int COLOR_AQUA = 0xFF00FFFF; + private static final int COLOR_WHITE = 0xFFFFFFFF; + private static final int COLOR_GRAY = 0xFF808080; + + // Parsed stat values + private float hp = Float.NaN; + private float maxHp = Float.NaN; + private float mana = Float.NaN; + private float maxMana = Float.NaN; + + // 0 = mana read OK, 1 = mana frozen/missing, 2 = NOT ENOUGH MANA + private int manaReadStatus = 0; + private float secInterval = 0.4f; + + // Interpolated bar heights + private float hpScale = 0; + private float manaScale = 0; + + // Animation cycle + private int ticks = 0; + private boolean cycle = false; + + private final ModConfig config; + + public StatsRingRenderer(ModConfig config) { + this.config = config; + } + + /** + * Register all Fabric event listeners. + */ + public void register() { + ClientReceiveMessageEvents.GAME.register(this::onGameMessage); + ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> resetState()); + ClientTickEvents.END_CLIENT_TICK.register(client -> onTick()); + HudRenderCallback.EVENT.register(this::onHudRender); + } + + private void onGameMessage(Component message, boolean overlay) { + if (!overlay) return; + if (!config.getActive() || !isInSkyBlock()) return; + + String msg = message.getString(); + + // Extract health information + if (msg.contains("\u2764")) { + String hpStats = msg.split("\u2764")[0]; + String[] hpParts = hpStats.split("/"); + if (hpParts.length >= 2) { + hp = parseStat(hpParts[0]); + maxHp = parseStat(hpParts[1]); + } + } + + // Extract mana information + if (msg.contains("\u270E")) { + manaReadStatus = 0; + String manaStats = msg.split("\u270E")[0]; + String[] p1 = manaStats.split("/"); + if (p1.length >= 2) { + maxMana = parseStat(p1[p1.length - 1]); + String[] p2 = p1[p1.length - 2].split(" "); + mana = parseStat(p2[p2.length - 1]); + } + } else { + manaReadStatus = msg.contains("NOT ENOUGH MANA") ? 2 : 1; + } + } + + private void resetState() { + hp = Float.NaN; + maxHp = Float.NaN; + mana = Float.NaN; + maxMana = Float.NaN; + hpScale = 0; + manaScale = 0; + ticks = 0; + cycle = false; + } + + private void onTick() { + ticks++; + if (ticks >= (int) (20 * secInterval)) { + cycle = !cycle; + ticks = 0; + } + } + + @SuppressWarnings("deprecation") + private void onHudRender(GuiGraphics graphics, DeltaTracker deltaTracker) { + boolean valuesAreNaN = Float.isNaN(hp) || Float.isNaN(maxHp) || Float.isNaN(mana) || Float.isNaN(maxMana); + if (!config.getActive() || valuesAreNaN || !isInSkyBlock()) return; + + Minecraft mc = Minecraft.getInstance(); + int xCenter = mc.getWindow().getGuiScaledWidth() / 2; + int yCenter = mc.getWindow().getGuiScaledHeight() / 2; + + // Draw backing ring image + if (config.getBackingImage()) { + int ringX = xCenter - RING_SIZE / 2; + int ringY = yCenter - RING_SIZE / 2; + graphics.blit(RenderType::guiTextured, RING_TEXTURE, ringX, ringY, 0, 0, RING_SIZE, RING_SIZE, RING_SIZE, RING_SIZE); + } + + // === Health bar === + float healthPercent = (hp / maxHp) * 100f; + if (!config.getAbsorption()) healthPercent = Math.min(100f, healthPercent); + + int hpColour; + if (healthPercent > 100f) { + // Absorption - gold + hpColour = argb(255, 255, 217, 0); + } else { + int r = config.getInterpolateColour() ? Math.round(lerp(139, 255, healthPercent / 100f)) : 255; + hpColour = argb(255, r, 0, 0); + } + + float currentHpScale = Math.min(HEIGHT_SCALE, HEIGHT_SCALE * (hp / maxHp)); + hpScale = config.getInterpolateBars() ? lerp(hpScale, currentHpScale, 0.1f) : currentHpScale; + int hpBarHeight = Math.round(hpScale); + + float lowHpPercent = config.getAlertLowHpPercent(); + if (lowHpPercent >= 0 && healthPercent <= lowHpPercent && cycle) { + // Flash "!!!" above HP bar + drawBoldText(graphics, mc, "!!!", xCenter - 14, yCenter - 22, hpColour); + hpColour = config.getInterpolateColour() ? COLOR_RED : COLOR_WHITE; + // Draw dark background for empty portion + graphics.fill(xCenter - 11, yCenter + 10 - HEIGHT_SCALE, xCenter - 11 + BAR_WIDTH, yCenter + 10 - hpBarHeight, darkenRgb(hpColour, 0.25f)); + } + + // Draw HP bar + graphics.fill(xCenter - 11, yCenter + 10 - hpBarHeight, xCenter - 11 + BAR_WIDTH, yCenter + 10, hpColour); + + // === Mana bar === + float manaPercentage = Math.min(100f, (mana / maxMana) * 100f); + + int manaColour; + if (config.getInterpolateColour()) { + int r = Math.round(lerp(200, 0, manaPercentage / 100f)); + int g = Math.round(lerp(100, 255, manaPercentage / 100f)); + manaColour = argb(255, r, g, 255); + } else { + manaColour = COLOR_AQUA; + } + + float currentManaScale = Math.min(HEIGHT_SCALE, HEIGHT_SCALE * (mana / maxMana)); + manaScale = config.getInterpolateBars() ? lerp(manaScale, currentManaScale, 0.1f) : currentManaScale; + int manaBarHeight = Math.round(manaScale); + + float lowManaPercent = config.getAlertLowManaPercent(); + secInterval = manaReadStatus == 2 ? 0.2f : 0.4f; + + if (manaReadStatus == 1) { + // Mana frozen - grey background + graphics.fill(xCenter + 9, yCenter + 10 - HEIGHT_SCALE, xCenter + 9 + BAR_WIDTH, yCenter + 10 - manaBarHeight, darkenRgb(COLOR_GRAY, 0.25f)); + } else if (((lowManaPercent >= 0 && manaPercentage <= lowManaPercent) || manaReadStatus == 2) && !cycle) { + // Flash "!!!" below mana bar + int foreColour = manaReadStatus == 2 ? COLOR_RED : (config.getInterpolateColour() ? COLOR_AQUA : COLOR_WHITE); + drawBoldText(graphics, mc, "!!!", xCenter + 7, yCenter + 13, foreColour); + manaColour = foreColour; + // Draw dark background + graphics.fill(xCenter + 9, yCenter + 10 - HEIGHT_SCALE, xCenter + 9 + BAR_WIDTH, yCenter + 10 - manaBarHeight, darkenRgb(manaColour, 0.25f)); + } + + // Draw mana bar + int manaBarColour = manaReadStatus != 1 ? manaColour : COLOR_GRAY; + graphics.fill(xCenter + 9, yCenter + 10 - manaBarHeight, xCenter + 9 + BAR_WIDTH, yCenter + 10, manaBarColour); + + // === Percentages === + if (!config.getPercentage()) return; + + float scale = 0.75f; + + // HP percentage + String hpText = Math.round(healthPercent) + "%"; + int hpTextX = Math.round(healthPercent) >= 100 ? xCenter - 32 : xCenter - 28; + int hpTextY = yCenter - Math.round(mc.font.lineHeight * scale / 2f); + graphics.pose().pushPose(); + graphics.pose().scale(scale, scale, 1.0f); + graphics.drawString(mc.font, hpText, Math.round(hpTextX / scale), Math.round(hpTextY / scale), hpColour, true); + graphics.pose().popPose(); + + // Mana percentage + int manaTextColour = manaReadStatus != 1 ? manaColour : COLOR_GRAY; + String manaText = Math.round(manaPercentage) + "%"; + int manaTextX = xCenter + 15; + int manaTextY = yCenter - Math.round(mc.font.lineHeight * scale / 2f); + graphics.pose().pushPose(); + graphics.pose().scale(scale, scale, 1.0f); + graphics.drawString(mc.font, manaText, Math.round(manaTextX / scale), Math.round(manaTextY / scale), manaTextColour, true); + graphics.pose().popPose(); + } + + // --- Utility methods --- + + private static boolean isInSkyBlock() { + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null) return false; + Scoreboard scoreboard = mc.level.getScoreboard(); + Objective objective = scoreboard.getDisplayObjective(DisplaySlot.SIDEBAR); + if (objective == null) return false; + return objective.getDisplayName().getString().contains("SKYBLOCK"); + } + + private static float parseStat(String stat) { + try { + return Float.parseFloat(stat.replaceAll("[^0-9]", "")); + } catch (NumberFormatException e) { + return Float.NaN; + } + } + + private static float lerp(float start, float end, float t) { + return start + (end - start) * t; + } + + private static int argb(int a, int r, int g, int b) { + return (a << 24) | (r << 16) | (g << 8) | b; + } + + private static int darkenRgb(int colour, float factor) { + int r = Math.round(((colour >> 16) & 0xFF) * factor); + int g = Math.round(((colour >> 8) & 0xFF) * factor); + int b = Math.round((colour & 0xFF) * factor); + return argb(255, r, g, b); + } + + private static void drawBoldText(GuiGraphics graphics, Minecraft mc, String text, int x, int y, int color) { + Component component = Component.literal(text).withStyle(Style.EMPTY.withBold(true)); + graphics.drawString(mc.font, component, x, y, color, true); + } +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index fdaf9d8..379cbd5 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -25,6 +25,7 @@ "fabricloader": ">=0.18.4", "minecraft": "~1.21.10", "java": ">=21", - "fabric-api": "*" + "fabric-api": "*", + "cloth-config": "*" } } \ No newline at end of file From 58090a556c7555bf4a0695ff5e59d78762f999df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 10:10:33 +0000 Subject: [PATCH 03/11] Fix Cloth Config version to 20.0.148 for MC 1.21.10 Co-authored-by: bubner <81782264+bubner@users.noreply.github.com> --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e1d3d4c..6e43a8f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,4 +18,4 @@ archives_base_name=statsring # Dependencies fabric_api_version=0.138.4+1.21.10 -cloth_config_version=17.0.144 \ No newline at end of file +cloth_config_version=20.0.148 \ No newline at end of file From 4e49b2bced035cbf30eaf2acdb9fed0a0423ccb1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 10:11:47 +0000 Subject: [PATCH 04/11] Address code review feedback - Add deprecation comment for HudRenderCallback - Fix parseStat to match original comma-stripping parseInt logic - Use ChatFormatting enum instead of raw formatting codes in title Co-authored-by: bubner <81782264+bubner@users.noreply.github.com> --- src/main/java/me/bubner/statsring/ConfigScreen.java | 8 +++++++- src/main/java/me/bubner/statsring/StatsRingRenderer.java | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/me/bubner/statsring/ConfigScreen.java b/src/main/java/me/bubner/statsring/ConfigScreen.java index 9cf45f2..299f690 100644 --- a/src/main/java/me/bubner/statsring/ConfigScreen.java +++ b/src/main/java/me/bubner/statsring/ConfigScreen.java @@ -3,6 +3,7 @@ import me.shedaniel.clothconfig2.api.ConfigBuilder; import me.shedaniel.clothconfig2.api.ConfigCategory; import me.shedaniel.clothconfig2.api.ConfigEntryBuilder; +import net.minecraft.ChatFormatting; import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; @@ -26,7 +27,12 @@ public static Screen create(Screen parent) { ModConfig config = StatsRing.CONFIG; ConfigBuilder builder = ConfigBuilder.create() .setParentScreen(parent) - .setTitle(Component.literal("\u00A7lStats Ring: \u00A7r\u00A7cHealth \u00A7rand \u00A7bMana \u00A7rring")); + .setTitle(Component.empty() + .append(Component.literal("Stats Ring: ").withStyle(ChatFormatting.BOLD)) + .append(Component.literal("Health ").withStyle(ChatFormatting.RED)) + .append(Component.literal("and ")) + .append(Component.literal("Mana ").withStyle(ChatFormatting.AQUA)) + .append(Component.literal("ring"))); ConfigEntryBuilder entryBuilder = builder.entryBuilder(); diff --git a/src/main/java/me/bubner/statsring/StatsRingRenderer.java b/src/main/java/me/bubner/statsring/StatsRingRenderer.java index 91510fe..8fc9f14 100644 --- a/src/main/java/me/bubner/statsring/StatsRingRenderer.java +++ b/src/main/java/me/bubner/statsring/StatsRingRenderer.java @@ -118,6 +118,7 @@ private void onTick() { } } + // HudRenderCallback is deprecated in favour of HudElementRegistry, but still functional @SuppressWarnings("deprecation") private void onHudRender(GuiGraphics graphics, DeltaTracker deltaTracker) { boolean valuesAreNaN = Float.isNaN(hp) || Float.isNaN(maxHp) || Float.isNaN(mana) || Float.isNaN(maxMana); @@ -236,7 +237,7 @@ private static boolean isInSkyBlock() { private static float parseStat(String stat) { try { - return Float.parseFloat(stat.replaceAll("[^0-9]", "")); + return Integer.parseInt(stat.replaceAll(",", "").trim()); } catch (NumberFormatException e) { return Float.NaN; } From bf31df0381fa29d9f2ebe710b41a0aca3a48c92b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 10:39:25 +0000 Subject: [PATCH 05/11] Address all PR review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix blit call: use graphics.blitSprite(RenderPipelines.GUI_TEXTURED, ...) instead of RenderType::guiTextured - Fix pose transforms: use Matrix3x2fStack with pushMatrix/popMatrix/translate/scale - Use HudElementRegistry.attachElementAfter(CROSSHAIR) instead of deprecated HudRenderCallback - Replace unicode escape sequences with actual characters (❤, ✎) - Extract utility methods into Util.java - Add ManaReadStatus enum to replace integer + comment - Use local var for config in StatsRing instead of static field - Remove init log message - Fix all @author tags to "2023 (Original CT module)" - Pass config as parameter to ConfigScreen.create() - Use Identifier instead of ResourceLocation for 1.21.10 Mojang mappings Co-authored-by: bubner <81782264+bubner@users.noreply.github.com> --- .../me/bubner/statsring/ConfigScreen.java | 6 +- .../me/bubner/statsring/ManaReadStatus.java | 12 ++ .../java/me/bubner/statsring/ModConfig.java | 2 +- .../java/me/bubner/statsring/StatsRing.java | 12 +- .../bubner/statsring/StatsRingRenderer.java | 134 ++++++------------ src/main/java/me/bubner/statsring/Util.java | 56 ++++++++ 6 files changed, 123 insertions(+), 99 deletions(-) create mode 100644 src/main/java/me/bubner/statsring/ManaReadStatus.java create mode 100644 src/main/java/me/bubner/statsring/Util.java diff --git a/src/main/java/me/bubner/statsring/ConfigScreen.java b/src/main/java/me/bubner/statsring/ConfigScreen.java index 299f690..bd4dac4 100644 --- a/src/main/java/me/bubner/statsring/ConfigScreen.java +++ b/src/main/java/me/bubner/statsring/ConfigScreen.java @@ -11,7 +11,7 @@ * Cloth Config screen for StatsRing settings. * Replaces the original Vigilance config GUI from config.js. * - * @author Lucas Bubner, 2023-2025 + * @author Lucas Bubner, 2023 (Original CT module) */ public class ConfigScreen { private ConfigScreen() { @@ -21,10 +21,10 @@ private ConfigScreen() { * Create the Cloth Config settings screen. * * @param parent the parent screen to return to + * @param config the mod configuration instance * @return the built config screen */ - public static Screen create(Screen parent) { - ModConfig config = StatsRing.CONFIG; + public static Screen create(Screen parent, ModConfig config) { ConfigBuilder builder = ConfigBuilder.create() .setParentScreen(parent) .setTitle(Component.empty() diff --git a/src/main/java/me/bubner/statsring/ManaReadStatus.java b/src/main/java/me/bubner/statsring/ManaReadStatus.java new file mode 100644 index 0000000..89851ed --- /dev/null +++ b/src/main/java/me/bubner/statsring/ManaReadStatus.java @@ -0,0 +1,12 @@ +package me.bubner.statsring; + +/** + * Represents the mana read status from the action bar. + * + * @author Lucas Bubner, 2023 (Original CT module) + */ +public enum ManaReadStatus { + OK, + FROZEN, + NOT_ENOUGH_MANA +} diff --git a/src/main/java/me/bubner/statsring/ModConfig.java b/src/main/java/me/bubner/statsring/ModConfig.java index 340623d..a166787 100644 --- a/src/main/java/me/bubner/statsring/ModConfig.java +++ b/src/main/java/me/bubner/statsring/ModConfig.java @@ -11,7 +11,7 @@ * Properties-based configuration for StatsRing. * Serialises settings to a .properties file in the Fabric config directory. * - * @author Lucas Bubner, 2023-2025 + * @author Lucas Bubner, 2023 (Original CT module) */ public class ModConfig { private static final Path CONFIG_PATH = FabricLoader.getInstance().getConfigDir().resolve("statsring.properties"); diff --git a/src/main/java/me/bubner/statsring/StatsRing.java b/src/main/java/me/bubner/statsring/StatsRing.java index 416a93e..376af08 100644 --- a/src/main/java/me/bubner/statsring/StatsRing.java +++ b/src/main/java/me/bubner/statsring/StatsRing.java @@ -12,28 +12,26 @@ * StatsRing - Display Health and Mana in a ring around the crosshair on Hypixel SkyBlock. * Ported from a 1.8.9 ChatTriggers module into a 1.21.10 Fabric mod. * - * @author Lucas Bubner, 2023-2025 + * @author Lucas Bubner, 2023 (Original CT module) */ public class StatsRing implements ClientModInitializer { public static final String MOD_ID = "statsring"; public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); - public static final ModConfig CONFIG = new ModConfig(); @Override public void onInitializeClient() { - CONFIG.load(); + final ModConfig config = new ModConfig(); + config.load(); - new StatsRingRenderer(CONFIG).register(); + new StatsRingRenderer(config).register(); // Register /ring command to open the settings GUI ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(ClientCommandManager.literal("ring").executes(context -> { Minecraft client = Minecraft.getInstance(); - client.execute(() -> client.setScreen(ConfigScreen.create(null))); + client.execute(() -> client.setScreen(ConfigScreen.create(null, config))); return 1; })) ); - - LOGGER.info("StatsRing initialised"); } } \ No newline at end of file diff --git a/src/main/java/me/bubner/statsring/StatsRingRenderer.java b/src/main/java/me/bubner/statsring/StatsRingRenderer.java index 8fc9f14..23b53dd 100644 --- a/src/main/java/me/bubner/statsring/StatsRingRenderer.java +++ b/src/main/java/me/bubner/statsring/StatsRingRenderer.java @@ -3,52 +3,49 @@ import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; -import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; +import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElement; +import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry; +import net.fabricmc.fabric.api.client.rendering.v1.hud.VanillaHudElements; import net.minecraft.client.DeltaTracker; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.RenderPipelines; import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.Style; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.scores.DisplaySlot; -import net.minecraft.world.scores.Objective; -import net.minecraft.world.scores.Scoreboard; +import net.minecraft.resources.Identifier; + +import org.joml.Matrix3x2fStack; + +import static me.bubner.statsring.Util.*; /** * Port of main.js from the ChatTriggers StatsRing module. * Handles action bar parsing for HP/Mana and renders the ring overlay on the HUD. * - * @author Lucas Bubner, 2023-2025 + * @author Lucas Bubner, 2023 (Original CT module) */ -public class StatsRingRenderer { - private static final ResourceLocation RING_TEXTURE = ResourceLocation.fromNamespaceAndPath("statsring", "ring-2.png"); +public class StatsRingRenderer implements HudElement { + private static final Identifier RING_TEXTURE = Identifier.fromNamespaceAndPath("statsring", "ring-2.png"); private static final int HEIGHT_SCALE = 21; private static final int RING_SIZE = 35; private static final int BAR_WIDTH = 3; - // Predefined colours (ARGB) private static final int COLOR_RED = 0xFFFF0000; private static final int COLOR_AQUA = 0xFF00FFFF; private static final int COLOR_WHITE = 0xFFFFFFFF; private static final int COLOR_GRAY = 0xFF808080; - // Parsed stat values private float hp = Float.NaN; private float maxHp = Float.NaN; private float mana = Float.NaN; private float maxMana = Float.NaN; - // 0 = mana read OK, 1 = mana frozen/missing, 2 = NOT ENOUGH MANA - private int manaReadStatus = 0; + private ManaReadStatus manaReadStatus = ManaReadStatus.OK; private float secInterval = 0.4f; - // Interpolated bar heights private float hpScale = 0; private float manaScale = 0; - // Animation cycle private int ticks = 0; private boolean cycle = false; @@ -65,7 +62,11 @@ public void register() { ClientReceiveMessageEvents.GAME.register(this::onGameMessage); ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> resetState()); ClientTickEvents.END_CLIENT_TICK.register(client -> onTick()); - HudRenderCallback.EVENT.register(this::onHudRender); + HudElementRegistry.attachElementAfter( + VanillaHudElements.CROSSHAIR, + Identifier.fromNamespaceAndPath("statsring", "ring"), + this + ); } private void onGameMessage(Component message, boolean overlay) { @@ -75,8 +76,8 @@ private void onGameMessage(Component message, boolean overlay) { String msg = message.getString(); // Extract health information - if (msg.contains("\u2764")) { - String hpStats = msg.split("\u2764")[0]; + if (msg.contains("❤")) { + String hpStats = msg.split("❤")[0]; String[] hpParts = hpStats.split("/"); if (hpParts.length >= 2) { hp = parseStat(hpParts[0]); @@ -85,9 +86,9 @@ private void onGameMessage(Component message, boolean overlay) { } // Extract mana information - if (msg.contains("\u270E")) { - manaReadStatus = 0; - String manaStats = msg.split("\u270E")[0]; + if (msg.contains("✎")) { + manaReadStatus = ManaReadStatus.OK; + String manaStats = msg.split("✎")[0]; String[] p1 = manaStats.split("/"); if (p1.length >= 2) { maxMana = parseStat(p1[p1.length - 1]); @@ -95,7 +96,7 @@ private void onGameMessage(Component message, boolean overlay) { mana = parseStat(p2[p2.length - 1]); } } else { - manaReadStatus = msg.contains("NOT ENOUGH MANA") ? 2 : 1; + manaReadStatus = msg.contains("NOT ENOUGH MANA") ? ManaReadStatus.NOT_ENOUGH_MANA : ManaReadStatus.FROZEN; } } @@ -118,9 +119,8 @@ private void onTick() { } } - // HudRenderCallback is deprecated in favour of HudElementRegistry, but still functional - @SuppressWarnings("deprecation") - private void onHudRender(GuiGraphics graphics, DeltaTracker deltaTracker) { + @Override + public void render(GuiGraphics graphics, DeltaTracker deltaTracker) { boolean valuesAreNaN = Float.isNaN(hp) || Float.isNaN(maxHp) || Float.isNaN(mana) || Float.isNaN(maxMana); if (!config.getActive() || valuesAreNaN || !isInSkyBlock()) return; @@ -132,7 +132,7 @@ private void onHudRender(GuiGraphics graphics, DeltaTracker deltaTracker) { if (config.getBackingImage()) { int ringX = xCenter - RING_SIZE / 2; int ringY = yCenter - RING_SIZE / 2; - graphics.blit(RenderType::guiTextured, RING_TEXTURE, ringX, ringY, 0, 0, RING_SIZE, RING_SIZE, RING_SIZE, RING_SIZE); + graphics.blitSprite(RenderPipelines.GUI_TEXTURED, RING_TEXTURE, ringX, ringY, RING_SIZE, RING_SIZE); } // === Health bar === @@ -141,7 +141,6 @@ private void onHudRender(GuiGraphics graphics, DeltaTracker deltaTracker) { int hpColour; if (healthPercent > 100f) { - // Absorption - gold hpColour = argb(255, 255, 217, 0); } else { int r = config.getInterpolateColour() ? Math.round(lerp(139, 255, healthPercent / 100f)) : 255; @@ -154,14 +153,11 @@ private void onHudRender(GuiGraphics graphics, DeltaTracker deltaTracker) { float lowHpPercent = config.getAlertLowHpPercent(); if (lowHpPercent >= 0 && healthPercent <= lowHpPercent && cycle) { - // Flash "!!!" above HP bar drawBoldText(graphics, mc, "!!!", xCenter - 14, yCenter - 22, hpColour); hpColour = config.getInterpolateColour() ? COLOR_RED : COLOR_WHITE; - // Draw dark background for empty portion graphics.fill(xCenter - 11, yCenter + 10 - HEIGHT_SCALE, xCenter - 11 + BAR_WIDTH, yCenter + 10 - hpBarHeight, darkenRgb(hpColour, 0.25f)); } - // Draw HP bar graphics.fill(xCenter - 11, yCenter + 10 - hpBarHeight, xCenter - 11 + BAR_WIDTH, yCenter + 10, hpColour); // === Mana bar === @@ -181,22 +177,18 @@ private void onHudRender(GuiGraphics graphics, DeltaTracker deltaTracker) { int manaBarHeight = Math.round(manaScale); float lowManaPercent = config.getAlertLowManaPercent(); - secInterval = manaReadStatus == 2 ? 0.2f : 0.4f; + secInterval = manaReadStatus == ManaReadStatus.NOT_ENOUGH_MANA ? 0.2f : 0.4f; - if (manaReadStatus == 1) { - // Mana frozen - grey background + if (manaReadStatus == ManaReadStatus.FROZEN) { graphics.fill(xCenter + 9, yCenter + 10 - HEIGHT_SCALE, xCenter + 9 + BAR_WIDTH, yCenter + 10 - manaBarHeight, darkenRgb(COLOR_GRAY, 0.25f)); - } else if (((lowManaPercent >= 0 && manaPercentage <= lowManaPercent) || manaReadStatus == 2) && !cycle) { - // Flash "!!!" below mana bar - int foreColour = manaReadStatus == 2 ? COLOR_RED : (config.getInterpolateColour() ? COLOR_AQUA : COLOR_WHITE); + } else if (((lowManaPercent >= 0 && manaPercentage <= lowManaPercent) || manaReadStatus == ManaReadStatus.NOT_ENOUGH_MANA) && !cycle) { + int foreColour = manaReadStatus == ManaReadStatus.NOT_ENOUGH_MANA ? COLOR_RED : (config.getInterpolateColour() ? COLOR_AQUA : COLOR_WHITE); drawBoldText(graphics, mc, "!!!", xCenter + 7, yCenter + 13, foreColour); manaColour = foreColour; - // Draw dark background graphics.fill(xCenter + 9, yCenter + 10 - HEIGHT_SCALE, xCenter + 9 + BAR_WIDTH, yCenter + 10 - manaBarHeight, darkenRgb(manaColour, 0.25f)); } - // Draw mana bar - int manaBarColour = manaReadStatus != 1 ? manaColour : COLOR_GRAY; + int manaBarColour = manaReadStatus != ManaReadStatus.FROZEN ? manaColour : COLOR_GRAY; graphics.fill(xCenter + 9, yCenter + 10 - manaBarHeight, xCenter + 9 + BAR_WIDTH, yCenter + 10, manaBarColour); // === Percentages === @@ -208,58 +200,24 @@ private void onHudRender(GuiGraphics graphics, DeltaTracker deltaTracker) { String hpText = Math.round(healthPercent) + "%"; int hpTextX = Math.round(healthPercent) >= 100 ? xCenter - 32 : xCenter - 28; int hpTextY = yCenter - Math.round(mc.font.lineHeight * scale / 2f); - graphics.pose().pushPose(); - graphics.pose().scale(scale, scale, 1.0f); - graphics.drawString(mc.font, hpText, Math.round(hpTextX / scale), Math.round(hpTextY / scale), hpColour, true); - graphics.pose().popPose(); + Matrix3x2fStack pose = graphics.pose(); + pose.pushMatrix(); + pose.translate(hpTextX, hpTextY); + pose.scale(scale); + pose.translate(-hpTextX, -hpTextY); + graphics.drawString(mc.font, hpText, hpTextX, hpTextY, hpColour, true); + pose.popMatrix(); // Mana percentage - int manaTextColour = manaReadStatus != 1 ? manaColour : COLOR_GRAY; + int manaTextColour = manaReadStatus != ManaReadStatus.FROZEN ? manaColour : COLOR_GRAY; String manaText = Math.round(manaPercentage) + "%"; int manaTextX = xCenter + 15; int manaTextY = yCenter - Math.round(mc.font.lineHeight * scale / 2f); - graphics.pose().pushPose(); - graphics.pose().scale(scale, scale, 1.0f); - graphics.drawString(mc.font, manaText, Math.round(manaTextX / scale), Math.round(manaTextY / scale), manaTextColour, true); - graphics.pose().popPose(); - } - - // --- Utility methods --- - - private static boolean isInSkyBlock() { - Minecraft mc = Minecraft.getInstance(); - if (mc.level == null) return false; - Scoreboard scoreboard = mc.level.getScoreboard(); - Objective objective = scoreboard.getDisplayObjective(DisplaySlot.SIDEBAR); - if (objective == null) return false; - return objective.getDisplayName().getString().contains("SKYBLOCK"); - } - - private static float parseStat(String stat) { - try { - return Integer.parseInt(stat.replaceAll(",", "").trim()); - } catch (NumberFormatException e) { - return Float.NaN; - } - } - - private static float lerp(float start, float end, float t) { - return start + (end - start) * t; - } - - private static int argb(int a, int r, int g, int b) { - return (a << 24) | (r << 16) | (g << 8) | b; - } - - private static int darkenRgb(int colour, float factor) { - int r = Math.round(((colour >> 16) & 0xFF) * factor); - int g = Math.round(((colour >> 8) & 0xFF) * factor); - int b = Math.round((colour & 0xFF) * factor); - return argb(255, r, g, b); - } - - private static void drawBoldText(GuiGraphics graphics, Minecraft mc, String text, int x, int y, int color) { - Component component = Component.literal(text).withStyle(Style.EMPTY.withBold(true)); - graphics.drawString(mc.font, component, x, y, color, true); + pose.pushMatrix(); + pose.translate(manaTextX, manaTextY); + pose.scale(scale); + pose.translate(-manaTextX, -manaTextY); + graphics.drawString(mc.font, manaText, manaTextX, manaTextY, manaTextColour, true); + pose.popMatrix(); } } diff --git a/src/main/java/me/bubner/statsring/Util.java b/src/main/java/me/bubner/statsring/Util.java new file mode 100644 index 0000000..a1307a0 --- /dev/null +++ b/src/main/java/me/bubner/statsring/Util.java @@ -0,0 +1,56 @@ +package me.bubner.statsring; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; +import net.minecraft.world.scores.DisplaySlot; +import net.minecraft.world.scores.Objective; +import net.minecraft.world.scores.Scoreboard; + +/** + * Shared utility methods for StatsRing rendering. + * + * @author Lucas Bubner, 2023 (Original CT module) + */ +public final class Util { + private Util() { + } + + public static boolean isInSkyBlock() { + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null) return false; + Scoreboard scoreboard = mc.level.getScoreboard(); + Objective objective = scoreboard.getDisplayObjective(DisplaySlot.SIDEBAR); + if (objective == null) return false; + return objective.getDisplayName().getString().contains("SKYBLOCK"); + } + + public static float parseStat(String stat) { + try { + return Integer.parseInt(stat.replaceAll(",", "").trim()); + } catch (NumberFormatException e) { + return Float.NaN; + } + } + + public static float lerp(float start, float end, float t) { + return start + (end - start) * t; + } + + public static int argb(int a, int r, int g, int b) { + return (a << 24) | (r << 16) | (g << 8) | b; + } + + public static int darkenRgb(int colour, float factor) { + int r = Math.round(((colour >> 16) & 0xFF) * factor); + int g = Math.round(((colour >> 8) & 0xFF) * factor); + int b = Math.round((colour & 0xFF) * factor); + return argb(255, r, g, b); + } + + public static void drawBoldText(GuiGraphics graphics, Minecraft mc, String text, int x, int y, int color) { + Component component = Component.literal(text).withStyle(Style.EMPTY.withBold(true)); + graphics.drawString(mc.font, component, x, y, color, true); + } +} From ba317eb894c8f1a3b80af8e24a65247ee4a18321 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 10:50:51 +0000 Subject: [PATCH 06/11] Fix Mojang mappings: Identifier->ResourceLocation, blitSprite->blit - Replace net.minecraft.resources.Identifier with ResourceLocation - Replace graphics.blitSprite() with graphics.blit() matching Mojang mappings API Co-authored-by: bubner <81782264+bubner@users.noreply.github.com> --- src/main/java/me/bubner/statsring/StatsRingRenderer.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/me/bubner/statsring/StatsRingRenderer.java b/src/main/java/me/bubner/statsring/StatsRingRenderer.java index 23b53dd..b97faef 100644 --- a/src/main/java/me/bubner/statsring/StatsRingRenderer.java +++ b/src/main/java/me/bubner/statsring/StatsRingRenderer.java @@ -11,7 +11,7 @@ import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.renderer.RenderPipelines; import net.minecraft.network.chat.Component; -import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceLocation; import org.joml.Matrix3x2fStack; @@ -24,7 +24,7 @@ * @author Lucas Bubner, 2023 (Original CT module) */ public class StatsRingRenderer implements HudElement { - private static final Identifier RING_TEXTURE = Identifier.fromNamespaceAndPath("statsring", "ring-2.png"); + private static final ResourceLocation RING_TEXTURE = ResourceLocation.fromNamespaceAndPath("statsring", "ring-2.png"); private static final int HEIGHT_SCALE = 21; private static final int RING_SIZE = 35; @@ -64,7 +64,7 @@ public void register() { ClientTickEvents.END_CLIENT_TICK.register(client -> onTick()); HudElementRegistry.attachElementAfter( VanillaHudElements.CROSSHAIR, - Identifier.fromNamespaceAndPath("statsring", "ring"), + ResourceLocation.fromNamespaceAndPath("statsring", "ring"), this ); } @@ -132,7 +132,7 @@ public void render(GuiGraphics graphics, DeltaTracker deltaTracker) { if (config.getBackingImage()) { int ringX = xCenter - RING_SIZE / 2; int ringY = yCenter - RING_SIZE / 2; - graphics.blitSprite(RenderPipelines.GUI_TEXTURED, RING_TEXTURE, ringX, ringY, RING_SIZE, RING_SIZE); + graphics.blit(RenderPipelines.GUI_TEXTURED, RING_TEXTURE, ringX, ringY, 0, 0, RING_SIZE, RING_SIZE, RING_SIZE, RING_SIZE); } // === Health bar === From d8047d49564b02dc1bc5c88f3530f199cff499cf Mon Sep 17 00:00:00 2001 From: Lucas Bubner Date: Mon, 9 Feb 2026 17:45:03 +1030 Subject: [PATCH 07/11] Fix Cloth Config --- README.md | 2 + .../java/me/bubner/statsring/StatsRing.java | 41 +++++++++++-------- .../bubner/statsring/StatsRingRenderer.java | 8 +--- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 3a8d744..f55148d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Display Health and Mana in a ring around the crosshair on Hypixel SkyBlock. **New:** Ported from a 1.8.9 ChatTriggers module into a 1.21.10 Fabric mod. +The Fabric version of StatsRing depends on the [Cloth Config API](https://modrinth.com/mod/cloth-config). + Optionally include percentages, visual alerts on low stats, and smooth animation. Useful for combat across SkyBlock. diff --git a/src/main/java/me/bubner/statsring/StatsRing.java b/src/main/java/me/bubner/statsring/StatsRing.java index 376af08..886bbec 100644 --- a/src/main/java/me/bubner/statsring/StatsRing.java +++ b/src/main/java/me/bubner/statsring/StatsRing.java @@ -3,8 +3,7 @@ import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; -import net.minecraft.client.Minecraft; - +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,23 +14,29 @@ * @author Lucas Bubner, 2023 (Original CT module) */ public class StatsRing implements ClientModInitializer { - public static final String MOD_ID = "statsring"; - public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + public static final String MOD_ID = "statsring"; + public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + private volatile boolean scheduleOpenConfigScreen = false; - @Override - public void onInitializeClient() { - final ModConfig config = new ModConfig(); - config.load(); + @Override + public void onInitializeClient() { + final ModConfig config = new ModConfig(); + config.load(); - new StatsRingRenderer(config).register(); + new StatsRingRenderer(config).register(); - // Register /ring command to open the settings GUI - ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> - dispatcher.register(ClientCommandManager.literal("ring").executes(context -> { - Minecraft client = Minecraft.getInstance(); - client.execute(() -> client.setScreen(ConfigScreen.create(null, config))); - return 1; - })) - ); - } + // Register /ring command to open the settings GUI + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> + dispatcher.register(ClientCommandManager.literal("ring").executes(context -> { + scheduleOpenConfigScreen = true; + return 1; + })) + ); + ClientTickEvents.END_CLIENT_TICK.register(client -> { + if (scheduleOpenConfigScreen) { + client.execute(() -> client.setScreen(ConfigScreen.create(client.screen, config))); + scheduleOpenConfigScreen = false; + } + }); + } } \ No newline at end of file diff --git a/src/main/java/me/bubner/statsring/StatsRingRenderer.java b/src/main/java/me/bubner/statsring/StatsRingRenderer.java index b97faef..e019576 100644 --- a/src/main/java/me/bubner/statsring/StatsRingRenderer.java +++ b/src/main/java/me/bubner/statsring/StatsRingRenderer.java @@ -12,7 +12,6 @@ import net.minecraft.client.renderer.RenderPipelines; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; - import org.joml.Matrix3x2fStack; import static me.bubner.statsring.Util.*; @@ -34,23 +33,18 @@ public class StatsRingRenderer implements HudElement { private static final int COLOR_AQUA = 0xFF00FFFF; private static final int COLOR_WHITE = 0xFFFFFFFF; private static final int COLOR_GRAY = 0xFF808080; - + private final ModConfig config; private float hp = Float.NaN; private float maxHp = Float.NaN; private float mana = Float.NaN; private float maxMana = Float.NaN; - private ManaReadStatus manaReadStatus = ManaReadStatus.OK; private float secInterval = 0.4f; - private float hpScale = 0; private float manaScale = 0; - private int ticks = 0; private boolean cycle = false; - private final ModConfig config; - public StatsRingRenderer(ModConfig config) { this.config = config; } From aecbc49af0b495193ae476c0f72956869831a18e Mon Sep 17 00:00:00 2001 From: Lucas Bubner Date: Mon, 9 Feb 2026 18:34:16 +1030 Subject: [PATCH 08/11] Fix parsing and util --- .../bubner/statsring/StatsRingRenderer.java | 51 +++++++++---------- src/main/java/me/bubner/statsring/Util.java | 3 +- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/main/java/me/bubner/statsring/StatsRingRenderer.java b/src/main/java/me/bubner/statsring/StatsRingRenderer.java index e019576..8a11f43 100644 --- a/src/main/java/me/bubner/statsring/StatsRingRenderer.java +++ b/src/main/java/me/bubner/statsring/StatsRingRenderer.java @@ -14,8 +14,6 @@ import net.minecraft.resources.ResourceLocation; import org.joml.Matrix3x2fStack; -import static me.bubner.statsring.Util.*; - /** * Port of main.js from the ChatTriggers StatsRing module. * Handles action bar parsing for HP/Mana and renders the ring overlay on the HUD. @@ -65,7 +63,7 @@ public void register() { private void onGameMessage(Component message, boolean overlay) { if (!overlay) return; - if (!config.getActive() || !isInSkyBlock()) return; + if (!config.getActive() || !Util.isInSkyBlock()) return; String msg = message.getString(); @@ -74,8 +72,8 @@ private void onGameMessage(Component message, boolean overlay) { String hpStats = msg.split("❤")[0]; String[] hpParts = hpStats.split("/"); if (hpParts.length >= 2) { - hp = parseStat(hpParts[0]); - maxHp = parseStat(hpParts[1]); + hp = Util.parseStat(hpParts[0]); + maxHp = Util.parseStat(hpParts[1]); } } @@ -83,11 +81,11 @@ private void onGameMessage(Component message, boolean overlay) { if (msg.contains("✎")) { manaReadStatus = ManaReadStatus.OK; String manaStats = msg.split("✎")[0]; - String[] p1 = manaStats.split("/"); - if (p1.length >= 2) { - maxMana = parseStat(p1[p1.length - 1]); - String[] p2 = p1[p1.length - 2].split(" "); - mana = parseStat(p2[p2.length - 1]); + String[] manaParts = manaStats.split("/"); + if (manaParts.length >= 2) { + maxMana = Util.parseStat(manaParts[manaParts.length - 1]); + String[] p2 = manaParts[manaParts.length - 2].split(" "); + mana = Util.parseStat(p2[p2.length - 1]); } } else { manaReadStatus = msg.contains("NOT ENOUGH MANA") ? ManaReadStatus.NOT_ENOUGH_MANA : ManaReadStatus.FROZEN; @@ -116,7 +114,7 @@ private void onTick() { @Override public void render(GuiGraphics graphics, DeltaTracker deltaTracker) { boolean valuesAreNaN = Float.isNaN(hp) || Float.isNaN(maxHp) || Float.isNaN(mana) || Float.isNaN(maxMana); - if (!config.getActive() || valuesAreNaN || !isInSkyBlock()) return; + if (!config.getActive() || valuesAreNaN || !Util.isInSkyBlock()) return; Minecraft mc = Minecraft.getInstance(); int xCenter = mc.getWindow().getGuiScaledWidth() / 2; @@ -135,21 +133,21 @@ public void render(GuiGraphics graphics, DeltaTracker deltaTracker) { int hpColour; if (healthPercent > 100f) { - hpColour = argb(255, 255, 217, 0); + hpColour = Util.argb(255, 255, 217, 0); } else { - int r = config.getInterpolateColour() ? Math.round(lerp(139, 255, healthPercent / 100f)) : 255; - hpColour = argb(255, r, 0, 0); + int r = config.getInterpolateColour() ? Math.round(Util.lerp(139, 255, healthPercent / 100f)) : 255; + hpColour = Util.argb(255, r, 0, 0); } float currentHpScale = Math.min(HEIGHT_SCALE, HEIGHT_SCALE * (hp / maxHp)); - hpScale = config.getInterpolateBars() ? lerp(hpScale, currentHpScale, 0.1f) : currentHpScale; + hpScale = config.getInterpolateBars() ? Util.lerp(hpScale, currentHpScale, 0.1f) : currentHpScale; int hpBarHeight = Math.round(hpScale); float lowHpPercent = config.getAlertLowHpPercent(); if (lowHpPercent >= 0 && healthPercent <= lowHpPercent && cycle) { - drawBoldText(graphics, mc, "!!!", xCenter - 14, yCenter - 22, hpColour); + Util.drawBoldText(graphics, mc, "!!!", xCenter - 14, yCenter - 22, hpColour); hpColour = config.getInterpolateColour() ? COLOR_RED : COLOR_WHITE; - graphics.fill(xCenter - 11, yCenter + 10 - HEIGHT_SCALE, xCenter - 11 + BAR_WIDTH, yCenter + 10 - hpBarHeight, darkenRgb(hpColour, 0.25f)); + graphics.fill(xCenter - 11, yCenter + 10 - HEIGHT_SCALE, xCenter - 11 + BAR_WIDTH, yCenter + 10 - hpBarHeight, Util.darkenRgb(hpColour, 0.25f)); } graphics.fill(xCenter - 11, yCenter + 10 - hpBarHeight, xCenter - 11 + BAR_WIDTH, yCenter + 10, hpColour); @@ -159,27 +157,27 @@ public void render(GuiGraphics graphics, DeltaTracker deltaTracker) { int manaColour; if (config.getInterpolateColour()) { - int r = Math.round(lerp(200, 0, manaPercentage / 100f)); - int g = Math.round(lerp(100, 255, manaPercentage / 100f)); - manaColour = argb(255, r, g, 255); + int r = Math.round(Util.lerp(200, 0, manaPercentage / 100f)); + int g = Math.round(Util.lerp(100, 255, manaPercentage / 100f)); + manaColour = Util.argb(255, r, g, 255); } else { manaColour = COLOR_AQUA; } float currentManaScale = Math.min(HEIGHT_SCALE, HEIGHT_SCALE * (mana / maxMana)); - manaScale = config.getInterpolateBars() ? lerp(manaScale, currentManaScale, 0.1f) : currentManaScale; + manaScale = config.getInterpolateBars() ? Util.lerp(manaScale, currentManaScale, 0.1f) : currentManaScale; int manaBarHeight = Math.round(manaScale); float lowManaPercent = config.getAlertLowManaPercent(); secInterval = manaReadStatus == ManaReadStatus.NOT_ENOUGH_MANA ? 0.2f : 0.4f; if (manaReadStatus == ManaReadStatus.FROZEN) { - graphics.fill(xCenter + 9, yCenter + 10 - HEIGHT_SCALE, xCenter + 9 + BAR_WIDTH, yCenter + 10 - manaBarHeight, darkenRgb(COLOR_GRAY, 0.25f)); + graphics.fill(xCenter + 9, yCenter + 10 - HEIGHT_SCALE, xCenter + 9 + BAR_WIDTH, yCenter + 10 - manaBarHeight, Util.darkenRgb(COLOR_GRAY, 0.25f)); } else if (((lowManaPercent >= 0 && manaPercentage <= lowManaPercent) || manaReadStatus == ManaReadStatus.NOT_ENOUGH_MANA) && !cycle) { int foreColour = manaReadStatus == ManaReadStatus.NOT_ENOUGH_MANA ? COLOR_RED : (config.getInterpolateColour() ? COLOR_AQUA : COLOR_WHITE); - drawBoldText(graphics, mc, "!!!", xCenter + 7, yCenter + 13, foreColour); + Util.drawBoldText(graphics, mc, "!!!", xCenter + 7, yCenter + 13, foreColour); manaColour = foreColour; - graphics.fill(xCenter + 9, yCenter + 10 - HEIGHT_SCALE, xCenter + 9 + BAR_WIDTH, yCenter + 10 - manaBarHeight, darkenRgb(manaColour, 0.25f)); + graphics.fill(xCenter + 9, yCenter + 10 - HEIGHT_SCALE, xCenter + 9 + BAR_WIDTH, yCenter + 10 - manaBarHeight, Util.darkenRgb(manaColour, 0.25f)); } int manaBarColour = manaReadStatus != ManaReadStatus.FROZEN ? manaColour : COLOR_GRAY; @@ -189,11 +187,12 @@ public void render(GuiGraphics graphics, DeltaTracker deltaTracker) { if (!config.getPercentage()) return; float scale = 0.75f; + int lineHeight = Math.round(mc.font.lineHeight * scale / 2f); // HP percentage String hpText = Math.round(healthPercent) + "%"; int hpTextX = Math.round(healthPercent) >= 100 ? xCenter - 32 : xCenter - 28; - int hpTextY = yCenter - Math.round(mc.font.lineHeight * scale / 2f); + int hpTextY = yCenter - lineHeight; Matrix3x2fStack pose = graphics.pose(); pose.pushMatrix(); pose.translate(hpTextX, hpTextY); @@ -206,7 +205,7 @@ public void render(GuiGraphics graphics, DeltaTracker deltaTracker) { int manaTextColour = manaReadStatus != ManaReadStatus.FROZEN ? manaColour : COLOR_GRAY; String manaText = Math.round(manaPercentage) + "%"; int manaTextX = xCenter + 15; - int manaTextY = yCenter - Math.round(mc.font.lineHeight * scale / 2f); + int manaTextY = yCenter - lineHeight; pose.pushMatrix(); pose.translate(manaTextX, manaTextY); pose.scale(scale); diff --git a/src/main/java/me/bubner/statsring/Util.java b/src/main/java/me/bubner/statsring/Util.java index a1307a0..c09320b 100644 --- a/src/main/java/me/bubner/statsring/Util.java +++ b/src/main/java/me/bubner/statsring/Util.java @@ -28,7 +28,8 @@ public static boolean isInSkyBlock() { public static float parseStat(String stat) { try { - return Integer.parseInt(stat.replaceAll(",", "").trim()); + // Remove any commas or format colours + return Integer.parseInt(stat.replaceAll("(,|§.|§$)", "").trim()); } catch (NumberFormatException e) { return Float.NaN; } From 121820b7b9251933aad268356ac09d81c0adfaff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 08:09:26 +0000 Subject: [PATCH 09/11] Extract magic numbers into named constants and add NOT_ENOUGH_MANA 2s latch - Move all positional magic numbers from render() into private static final constants: HP_BAR_X_OFFSET, MANA_BAR_X_OFFSET, BAR_BOTTOM_Y_OFFSET, HP_ALERT_X/Y_OFFSET, MANA_ALERT_X/Y_OFFSET, HP_PERCENT_X_OFFSET(_WIDE), MANA_PERCENT_X_OFFSET - Move float scale = 0.75f to PERCENT_SCALE constant - Add NOT_ENOUGH_MANA latch: when mana status reads NOT_ENOUGH_MANA, the warning state persists for 2 seconds (40 ticks) via notEnoughManaLatchTicks counter, preventing premature reset when new action bar messages arrive Co-authored-by: bubner <81782264+bubner@users.noreply.github.com> --- .../bubner/statsring/StatsRingRenderer.java | 61 ++++++++++++++----- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/src/main/java/me/bubner/statsring/StatsRingRenderer.java b/src/main/java/me/bubner/statsring/StatsRingRenderer.java index 8a11f43..e921b2b 100644 --- a/src/main/java/me/bubner/statsring/StatsRingRenderer.java +++ b/src/main/java/me/bubner/statsring/StatsRingRenderer.java @@ -26,17 +26,35 @@ public class StatsRingRenderer implements HudElement { private static final int HEIGHT_SCALE = 21; private static final int RING_SIZE = 35; private static final int BAR_WIDTH = 3; + private static final float PERCENT_SCALE = 0.75f; + + private static final int HP_BAR_X_OFFSET = -11; + private static final int MANA_BAR_X_OFFSET = 9; + private static final int BAR_BOTTOM_Y_OFFSET = 10; + + private static final int HP_ALERT_X_OFFSET = -14; + private static final int HP_ALERT_Y_OFFSET = -22; + private static final int MANA_ALERT_X_OFFSET = 7; + private static final int MANA_ALERT_Y_OFFSET = 13; + + private static final int HP_PERCENT_X_OFFSET_WIDE = -32; + private static final int HP_PERCENT_X_OFFSET = -28; + private static final int MANA_PERCENT_X_OFFSET = 15; + + private static final int NOT_ENOUGH_MANA_LATCH_TICKS = 40; private static final int COLOR_RED = 0xFFFF0000; private static final int COLOR_AQUA = 0xFF00FFFF; private static final int COLOR_WHITE = 0xFFFFFFFF; private static final int COLOR_GRAY = 0xFF808080; + private final ModConfig config; private float hp = Float.NaN; private float maxHp = Float.NaN; private float mana = Float.NaN; private float maxMana = Float.NaN; private ManaReadStatus manaReadStatus = ManaReadStatus.OK; + private int notEnoughManaLatchTicks = 0; private float secInterval = 0.4f; private float hpScale = 0; private float manaScale = 0; @@ -79,7 +97,9 @@ private void onGameMessage(Component message, boolean overlay) { // Extract mana information if (msg.contains("✎")) { - manaReadStatus = ManaReadStatus.OK; + if (notEnoughManaLatchTicks <= 0) { + manaReadStatus = ManaReadStatus.OK; + } String manaStats = msg.split("✎")[0]; String[] manaParts = manaStats.split("/"); if (manaParts.length >= 2) { @@ -88,7 +108,12 @@ private void onGameMessage(Component message, boolean overlay) { mana = Util.parseStat(p2[p2.length - 1]); } } else { - manaReadStatus = msg.contains("NOT ENOUGH MANA") ? ManaReadStatus.NOT_ENOUGH_MANA : ManaReadStatus.FROZEN; + if (msg.contains("NOT ENOUGH MANA")) { + manaReadStatus = ManaReadStatus.NOT_ENOUGH_MANA; + notEnoughManaLatchTicks = NOT_ENOUGH_MANA_LATCH_TICKS; + } else if (notEnoughManaLatchTicks <= 0) { + manaReadStatus = ManaReadStatus.FROZEN; + } } } @@ -101,6 +126,7 @@ private void resetState() { manaScale = 0; ticks = 0; cycle = false; + notEnoughManaLatchTicks = 0; } private void onTick() { @@ -109,6 +135,12 @@ private void onTick() { cycle = !cycle; ticks = 0; } + if (notEnoughManaLatchTicks > 0) { + notEnoughManaLatchTicks--; + if (notEnoughManaLatchTicks <= 0) { + manaReadStatus = ManaReadStatus.OK; + } + } } @Override @@ -145,12 +177,12 @@ public void render(GuiGraphics graphics, DeltaTracker deltaTracker) { float lowHpPercent = config.getAlertLowHpPercent(); if (lowHpPercent >= 0 && healthPercent <= lowHpPercent && cycle) { - Util.drawBoldText(graphics, mc, "!!!", xCenter - 14, yCenter - 22, hpColour); + Util.drawBoldText(graphics, mc, "!!!", xCenter + HP_ALERT_X_OFFSET, yCenter + HP_ALERT_Y_OFFSET, hpColour); hpColour = config.getInterpolateColour() ? COLOR_RED : COLOR_WHITE; - graphics.fill(xCenter - 11, yCenter + 10 - HEIGHT_SCALE, xCenter - 11 + BAR_WIDTH, yCenter + 10 - hpBarHeight, Util.darkenRgb(hpColour, 0.25f)); + graphics.fill(xCenter + HP_BAR_X_OFFSET, yCenter + BAR_BOTTOM_Y_OFFSET - HEIGHT_SCALE, xCenter + HP_BAR_X_OFFSET + BAR_WIDTH, yCenter + BAR_BOTTOM_Y_OFFSET - hpBarHeight, Util.darkenRgb(hpColour, 0.25f)); } - graphics.fill(xCenter - 11, yCenter + 10 - hpBarHeight, xCenter - 11 + BAR_WIDTH, yCenter + 10, hpColour); + graphics.fill(xCenter + HP_BAR_X_OFFSET, yCenter + BAR_BOTTOM_Y_OFFSET - hpBarHeight, xCenter + HP_BAR_X_OFFSET + BAR_WIDTH, yCenter + BAR_BOTTOM_Y_OFFSET, hpColour); // === Mana bar === float manaPercentage = Math.min(100f, (mana / maxMana) * 100f); @@ -172,31 +204,30 @@ public void render(GuiGraphics graphics, DeltaTracker deltaTracker) { secInterval = manaReadStatus == ManaReadStatus.NOT_ENOUGH_MANA ? 0.2f : 0.4f; if (manaReadStatus == ManaReadStatus.FROZEN) { - graphics.fill(xCenter + 9, yCenter + 10 - HEIGHT_SCALE, xCenter + 9 + BAR_WIDTH, yCenter + 10 - manaBarHeight, Util.darkenRgb(COLOR_GRAY, 0.25f)); + graphics.fill(xCenter + MANA_BAR_X_OFFSET, yCenter + BAR_BOTTOM_Y_OFFSET - HEIGHT_SCALE, xCenter + MANA_BAR_X_OFFSET + BAR_WIDTH, yCenter + BAR_BOTTOM_Y_OFFSET - manaBarHeight, Util.darkenRgb(COLOR_GRAY, 0.25f)); } else if (((lowManaPercent >= 0 && manaPercentage <= lowManaPercent) || manaReadStatus == ManaReadStatus.NOT_ENOUGH_MANA) && !cycle) { int foreColour = manaReadStatus == ManaReadStatus.NOT_ENOUGH_MANA ? COLOR_RED : (config.getInterpolateColour() ? COLOR_AQUA : COLOR_WHITE); - Util.drawBoldText(graphics, mc, "!!!", xCenter + 7, yCenter + 13, foreColour); + Util.drawBoldText(graphics, mc, "!!!", xCenter + MANA_ALERT_X_OFFSET, yCenter + MANA_ALERT_Y_OFFSET, foreColour); manaColour = foreColour; - graphics.fill(xCenter + 9, yCenter + 10 - HEIGHT_SCALE, xCenter + 9 + BAR_WIDTH, yCenter + 10 - manaBarHeight, Util.darkenRgb(manaColour, 0.25f)); + graphics.fill(xCenter + MANA_BAR_X_OFFSET, yCenter + BAR_BOTTOM_Y_OFFSET - HEIGHT_SCALE, xCenter + MANA_BAR_X_OFFSET + BAR_WIDTH, yCenter + BAR_BOTTOM_Y_OFFSET - manaBarHeight, Util.darkenRgb(manaColour, 0.25f)); } int manaBarColour = manaReadStatus != ManaReadStatus.FROZEN ? manaColour : COLOR_GRAY; - graphics.fill(xCenter + 9, yCenter + 10 - manaBarHeight, xCenter + 9 + BAR_WIDTH, yCenter + 10, manaBarColour); + graphics.fill(xCenter + MANA_BAR_X_OFFSET, yCenter + BAR_BOTTOM_Y_OFFSET - manaBarHeight, xCenter + MANA_BAR_X_OFFSET + BAR_WIDTH, yCenter + BAR_BOTTOM_Y_OFFSET, manaBarColour); // === Percentages === if (!config.getPercentage()) return; - float scale = 0.75f; - int lineHeight = Math.round(mc.font.lineHeight * scale / 2f); + int lineHeight = Math.round(mc.font.lineHeight * PERCENT_SCALE / 2f); // HP percentage String hpText = Math.round(healthPercent) + "%"; - int hpTextX = Math.round(healthPercent) >= 100 ? xCenter - 32 : xCenter - 28; + int hpTextX = Math.round(healthPercent) >= 100 ? xCenter + HP_PERCENT_X_OFFSET_WIDE : xCenter + HP_PERCENT_X_OFFSET; int hpTextY = yCenter - lineHeight; Matrix3x2fStack pose = graphics.pose(); pose.pushMatrix(); pose.translate(hpTextX, hpTextY); - pose.scale(scale); + pose.scale(PERCENT_SCALE); pose.translate(-hpTextX, -hpTextY); graphics.drawString(mc.font, hpText, hpTextX, hpTextY, hpColour, true); pose.popMatrix(); @@ -204,11 +235,11 @@ public void render(GuiGraphics graphics, DeltaTracker deltaTracker) { // Mana percentage int manaTextColour = manaReadStatus != ManaReadStatus.FROZEN ? manaColour : COLOR_GRAY; String manaText = Math.round(manaPercentage) + "%"; - int manaTextX = xCenter + 15; + int manaTextX = xCenter + MANA_PERCENT_X_OFFSET; int manaTextY = yCenter - lineHeight; pose.pushMatrix(); pose.translate(manaTextX, manaTextY); - pose.scale(scale); + pose.scale(PERCENT_SCALE); pose.translate(-manaTextX, -manaTextY); graphics.drawString(mc.font, manaText, manaTextX, manaTextY, manaTextColour, true); pose.popMatrix(); From 54e4864a874d24723f84dd405f5e3e01f6a11e44 Mon Sep 17 00:00:00 2001 From: Lucas Bubner Date: Mon, 9 Feb 2026 19:28:30 +1030 Subject: [PATCH 10/11] UI tuning --- .../bubner/statsring/StatsRingRenderer.java | 6 +++--- .../resources/assets/statsring/ring-2.png | Bin 3586 -> 3110 bytes 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/me/bubner/statsring/StatsRingRenderer.java b/src/main/java/me/bubner/statsring/StatsRingRenderer.java index e921b2b..bfbf59a 100644 --- a/src/main/java/me/bubner/statsring/StatsRingRenderer.java +++ b/src/main/java/me/bubner/statsring/StatsRingRenderer.java @@ -29,17 +29,17 @@ public class StatsRingRenderer implements HudElement { private static final float PERCENT_SCALE = 0.75f; private static final int HP_BAR_X_OFFSET = -11; - private static final int MANA_BAR_X_OFFSET = 9; + private static final int MANA_BAR_X_OFFSET = 7; private static final int BAR_BOTTOM_Y_OFFSET = 10; private static final int HP_ALERT_X_OFFSET = -14; private static final int HP_ALERT_Y_OFFSET = -22; - private static final int MANA_ALERT_X_OFFSET = 7; + private static final int MANA_ALERT_X_OFFSET = 5; private static final int MANA_ALERT_Y_OFFSET = 13; private static final int HP_PERCENT_X_OFFSET_WIDE = -32; private static final int HP_PERCENT_X_OFFSET = -28; - private static final int MANA_PERCENT_X_OFFSET = 15; + private static final int MANA_PERCENT_X_OFFSET = 13; private static final int NOT_ENOUGH_MANA_LATCH_TICKS = 40; diff --git a/src/main/resources/assets/statsring/ring-2.png b/src/main/resources/assets/statsring/ring-2.png index 836aa019413f5b833adaf41f62fba7f8284a50f8..3224dd9518678ac702d2a31cecbdd3bfb84a2379 100644 GIT binary patch literal 3110 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 literal 3586 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 Date: Mon, 9 Feb 2026 20:00:28 +1030 Subject: [PATCH 11/11] Precompile pattern --- src/main/java/me/bubner/statsring/Util.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/me/bubner/statsring/Util.java b/src/main/java/me/bubner/statsring/Util.java index c09320b..4991bb2 100644 --- a/src/main/java/me/bubner/statsring/Util.java +++ b/src/main/java/me/bubner/statsring/Util.java @@ -8,12 +8,16 @@ import net.minecraft.world.scores.Objective; import net.minecraft.world.scores.Scoreboard; +import java.util.regex.Pattern; + /** * Shared utility methods for StatsRing rendering. * * @author Lucas Bubner, 2023 (Original CT module) */ public final class Util { + private static final Pattern COMMAS_FORMAT_COLOURS = Pattern.compile("(,|§.|§$)"); + private Util() { } @@ -29,7 +33,7 @@ public static boolean isInSkyBlock() { public static float parseStat(String stat) { try { // Remove any commas or format colours - return Integer.parseInt(stat.replaceAll("(,|§.|§$)", "").trim()); + return Integer.parseInt(COMMAS_FORMAT_COLOURS.matcher(stat).replaceAll("").trim()); } catch (NumberFormatException e) { return Float.NaN; }