From bd11af299df07254f876a717dc24b4b6fdbde68b Mon Sep 17 00:00:00 2001 From: gaoyu06 Date: Fri, 31 Jul 2026 10:08:14 -0700 Subject: [PATCH] =?UTF-8?q?refactor(hud):=20=E5=BC=95=E5=85=A5=20measure?= =?UTF-8?q?=20=E9=98=B6=E6=AE=B5=E5=B9=B6=E8=AE=A9=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E5=A3=B0=E6=98=8E=E8=83=8C=E6=99=AF=E5=87=A0=E4=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 组件的 width/height 一直是在 draw() 内部赋值的,而下列路径全部在 draw() 之前 读取它们:锚点定位(getRealPosition)、hover 检测、拖拽边界钳制、blur 蒙版。 结果是所有组件的这些行为都滞后一帧 —— FPS 从 99fps 跳到 100fps 时右对齐组件的 右边缘会抖,进世界第一帧 width 还是 0,右对齐组件会闪现在屏幕最右缘。 新增 Component.measure()(默认空实现,未迁移的组件保持原状),由 ComponentsManager.measureAll() 在 GlobalListener 每帧开头统一调用一次, 早于 blur 蒙版与绘制两个 pass。 背景几何声明:新增 Component.backgroundShape(ShapeSink, originX, originY), drawBlurMask 据此生成蒙版而不再猜。此前蒙版假设「每个组件都是位于 (rX-2, rY)、 尺寸 width×height 的单个矩形」,这对约三分之二的组件成立,其余要么原点不同 (Keystrokes、PotionDisplay),要么画的是多个互不相连的方块(ArmorDisplay), 于是模糊会从缝隙里透出来或整体偏 2px。 - Keystrokes 声明每个键位框,并把 spacing 布局计算从 draw() 移到 measure(), 使蒙版拿到当帧偏移;键宽抽成 Key.renderWidth() 供 render 与 shape 共用, 避免空格键在两处算出不同宽度 - ArmorDisplay 声明 4 个槽位方块,槽位偏移抽成 slotOffset() 共用。顺带修复 该偏移此前未乘 scale —— 放大 HUD 时槽位会互相重叠 抽出 TextComponent:九个组件重复着逐字符相同的四行(量文字、定高、画框、画字), 差异只有字号、padding、框高、文字偏移。改为模板方法后每个降到 3-5 行,并自动 获得正确的 measure 时序。已迁移 FPS/CPS/Combo/Reach/Coords/Ping/Clock/PlayTime/ ServerAddress。 其中三个组件各自持有一份逐字符相同的 getLabel(),且都在 draw() 里对设置调用 setValue 写回默认值 —— 那会触发 EventValueChange 进而存盘,位于渲染路径上。 统一为 TextComponent.resolveLabel() 并去掉写回:设置为空时每帧重新解析即可, 显示结果不变。 同时补上几处缺失的 null 检查(CoordsDisplay/PingDisplay 此前直接解引用 mc.thePlayer),并让 TextComponent 在无内容时把尺寸归零,避免留下过期尺寸 让蒙版画出一个已经不存在的面板。 Co-Authored-By: Claude --- .../fpsmaster/features/GlobalListener.java | 4 + .../top/fpsmaster/ui/custom/Component.java | 64 +++++++++++-- .../ui/custom/ComponentsManager.java | 18 ++++ .../fpsmaster/ui/custom/TextComponent.java | 93 +++++++++++++++++++ .../ui/custom/impl/ArmorDisplayComponent.java | 70 ++++++++------ .../ui/custom/impl/CPSDisplayComponent.java | 26 +++--- .../ui/custom/impl/ClockDisplayComponent.java | 56 ++++++----- .../ui/custom/impl/ComboDisplayComponent.java | 36 ++++--- .../custom/impl/CoordsDisplayComponent.java | 44 ++++----- .../ui/custom/impl/FPSDisplayComponent.java | 26 +++--- .../ui/custom/impl/KeystrokesComponent.java | 71 +++++++++----- .../ui/custom/impl/PingDisplayComponent.java | 32 +++---- .../ui/custom/impl/PlayTimeComponent.java | 54 ++++++----- .../ui/custom/impl/ReachDisplayComponent.java | 24 ++--- .../impl/ServerAddressDisplayComponent.java | 60 ++++++------ 15 files changed, 458 insertions(+), 220 deletions(-) create mode 100644 src/main/java/top/fpsmaster/ui/custom/TextComponent.java diff --git a/src/main/java/top/fpsmaster/features/GlobalListener.java b/src/main/java/top/fpsmaster/features/GlobalListener.java index 30c1517f..9efe172a 100644 --- a/src/main/java/top/fpsmaster/features/GlobalListener.java +++ b/src/main/java/top/fpsmaster/features/GlobalListener.java @@ -75,6 +75,10 @@ public void onRender(EventRender2D e) { float mouseX = (float) Mouse.getX() / scaledResolution.getScaleFactor(); float mouseY = scaledResolution.getScaledHeight() - (float) Mouse.getY() / scaledResolution.getScaleFactor(); + // Size everything first: the blur mask and the anchor math below both read width/height, and + // components only assign those while drawing. + FPSMaster.componentsManager.measureAll(); + if (ClientSettings.blur.getValue()) { StencilUtil.initStencilToWrite(); EventDispatcher.dispatchEvent(new EventShader()); diff --git a/src/main/java/top/fpsmaster/ui/custom/Component.java b/src/main/java/top/fpsmaster/ui/custom/Component.java index d7b0aa79..e156f9f2 100644 --- a/src/main/java/top/fpsmaster/ui/custom/Component.java +++ b/src/main/java/top/fpsmaster/ui/custom/Component.java @@ -71,6 +71,48 @@ public Component(Class clazz) { public void draw(float x, float y) { } + /** + * Computes {@link #width}/{@link #height} for this frame, before anything reads them. + * + *

Historically every component assigned its size inside {@code draw()}, but anchoring + * ({@link #getRealPosition}), hover testing, drag clamping and the blur mask all run before + * {@code draw()} — so they used the previous frame's size. A component whose size changes each + * frame (FPS going {@code 99fps} → {@code 100fps}, a potion expiring) visibly jitters, and on its + * very first frame {@code width} is still 0. + * + *

Default implementation does nothing, so components that have not been migrated keep their old + * behaviour. Override it, move the sizing math here, and {@code draw()} becomes pure rendering. + */ + public void measure() { + } + + /** Receives the rectangles that make up a component's background. */ + public interface ShapeSink { + /** + * @param x absolute left edge, already scaled by the caller (same convention as + * {@link #drawRect}: positions are the caller's job, sizes are the base class's) + * @param y absolute top edge + * @param width logical width; the base class multiplies by {@link #scale} + * @param height logical height + */ + void rect(float x, float y, float width, float height); + } + + /** + * Declares the geometry of this component's background so the blur mask can reproduce it exactly + * instead of guessing. + * + *

The mask used to assume every component draws one rectangle at {@code (rX - 2, rY)} sized + * {@code width × height}. That holds for about two thirds of them; the rest either use a different + * origin (Keystrokes, PotionDisplay) or draw several disjoint boxes (ArmorDisplay), so the blur + * leaked into the gaps or sat a couple of pixels off. + * + *

Whatever a component declares here must match what it actually paints in {@code draw()}. + */ + public void backgroundShape(ShapeSink sink, float originX, float originY) { + sink.rect(originX - 2f, originY, width, height); + } + public float alpha = 0f; public boolean shouldDisplay() { @@ -131,13 +173,21 @@ public void drawBlurMask(ScaledResolution sr) { return; } float[] pos = getRealPosition(sr); - float scaledWidth = width * scale; - float scaledHeight = height * scale; - if (mod.rounded.getValue()) { - Rects.roundedImage(Math.round(pos[0] - 2), Math.round(pos[1]), Math.round(scaledWidth), Math.round(scaledHeight), mod.roundRadius.getValue().intValue(), STENCIL_MASK_COLOR); - } else { - Rects.fill(pos[0] - 2, pos[1], scaledWidth, scaledHeight, STENCIL_MASK_COLOR); - } + boolean round = mod.rounded.getValue(); + int radius = mod.roundRadius.getValue().intValue(); + // Same geometry the component paints, so the mask can never drift from the background. + backgroundShape((rectX, rectY, rectWidth, rectHeight) -> { + float scaledWidth = rectWidth * scale; + float scaledHeight = rectHeight * scale; + if (scaledWidth <= 0f || scaledHeight <= 0f) { + return; + } + if (round) { + Rects.roundedImage(Math.round(rectX), Math.round(rectY), Math.round(scaledWidth), Math.round(scaledHeight), radius, STENCIL_MASK_COLOR); + } else { + Rects.fill(rectX, rectY, scaledWidth, scaledHeight, STENCIL_MASK_COLOR); + } + }, pos[0], pos[1]); } public void display(ScaledResolution sr, int mouseX, int mouseY) { diff --git a/src/main/java/top/fpsmaster/ui/custom/ComponentsManager.java b/src/main/java/top/fpsmaster/ui/custom/ComponentsManager.java index 76072c84..db3211f7 100644 --- a/src/main/java/top/fpsmaster/ui/custom/ComponentsManager.java +++ b/src/main/java/top/fpsmaster/ui/custom/ComponentsManager.java @@ -94,6 +94,24 @@ private void resetRenderState() { GlStateManager.color(1f, 1f, 1f, 1f); } + /** + * Sizes every visible component for this frame. Must run before anything reads width/height — + * anchoring, hover testing, drag clamping and the blur mask all do, and all of them used to see + * the previous frame's values. Called once per frame from GlobalListener, ahead of both the blur + * mask pass and the draw pass. + */ + public void measureAll() { + components.forEach(component -> { + if (component.shouldDisplay()) { + try { + component.measure(); + } catch (Throwable throwable) { + onComponentFailure(component, "measure", throwable); + } + } + }); + } + public void drawBackgroundMasks() { GL11.glPushMatrix(); net.minecraft.client.gui.ScaledResolution sr = new net.minecraft.client.gui.ScaledResolution(Utility.mc); diff --git a/src/main/java/top/fpsmaster/ui/custom/TextComponent.java b/src/main/java/top/fpsmaster/ui/custom/TextComponent.java new file mode 100644 index 00000000..15302349 --- /dev/null +++ b/src/main/java/top/fpsmaster/ui/custom/TextComponent.java @@ -0,0 +1,93 @@ +package top.fpsmaster.ui.custom; + +import top.fpsmaster.FPSMaster; + +/** + * A HUD component that is one background panel with a single line of text on it. + * + *

Nine components repeated the same four statements verbatim — measure the string, set a fixed + * height, paint the panel, paint the text — differing only in font size, padding, box height and text + * offset. Expressing that as a template also fixes the sizing order for free: {@link #measure()} runs + * before anything reads {@code width}/{@code height}, instead of the size being assigned midway + * through {@code draw()}. + */ +public abstract class TextComponent extends Component { + + private String measured; + + protected TextComponent(Class clazz) { + super(clazz); + } + + /** The line to render, or {@code null} when there is nothing to show this frame. */ + protected abstract String text(); + + protected abstract int fontSize(); + + protected abstract int textColor(); + + protected float horizontalPadding() { + return 4f; + } + + protected float boxHeight() { + return 14f; + } + + protected float textOffsetX() { + return 0f; + } + + protected float textOffsetY() { + return 2f; + } + + /** + * Resolves a user-editable label, falling back to the translation and then to a built-in string. + * + *

Three components carried a private copy of this. Each also wrote the resolved value back with + * {@code setValue} from inside {@code draw()}, which fires EventValueChange and therefore a config + * save — from the render path, potentially every frame. Resolving without persisting keeps the same + * display and drops the write entirely: an empty setting simply resolves again next frame. + * + * @param i18nKey translation key; {@code Language.get} returns the key itself when unmapped + * @param fallback used when the key has no translation + */ + protected String resolveLabel(String configured, String i18nKey, String fallback) { + String label = configured; + if (label == null || label.trim().isEmpty()) { + label = FPSMaster.i18n.get(i18nKey); + if (i18nKey.equals(label)) { + label = fallback; + } + } + if (!label.endsWith(":") && !label.endsWith(":") && !label.endsWith(" ")) { + label += ": "; + } + return label; + } + + @Override + public void measure() { + measured = text(); + if (measured == null) { + // Zero the size rather than leaving last frame's: a stale width would have the blur mask + // stamp a panel this component is no longer drawing. + width = 0f; + height = 0f; + return; + } + width = getStringWidth(fontSize(), measured) + horizontalPadding(); + height = boxHeight(); + } + + @Override + public void draw(float x, float y) { + if (measured == null) { + return; + } + drawRect(x - 2, y, width, height, mod.backgroundColor.getColor()); + // Offsets are positions, so they scale; drawString scales the glyphs itself. + drawString(fontSize(), measured, x + textOffsetX() * scale, y + textOffsetY() * scale, textColor()); + } +} diff --git a/src/main/java/top/fpsmaster/ui/custom/impl/ArmorDisplayComponent.java b/src/main/java/top/fpsmaster/ui/custom/impl/ArmorDisplayComponent.java index 049de81c..e63b0c32 100644 --- a/src/main/java/top/fpsmaster/ui/custom/impl/ArmorDisplayComponent.java +++ b/src/main/java/top/fpsmaster/ui/custom/impl/ArmorDisplayComponent.java @@ -19,29 +19,56 @@ public ArmorDisplayComponent() { allowScale = true; } + private static final int SLOTS = 4; + private static final float SLOT_SIZE = 16f; + + @Override + public void measure() { + float spacing = mod.spacing.getValue().floatValue(); + if (ArmorDisplay.mode.getValue() == 0) { + width = 70f + spacing; + height = 18f; + } else { + width = 70f; + height = 4 + spacing + SLOTS * SLOT_SIZE; + } + } + + /** + * Top-left of slot {@code index}, already scaled. Shared by draw() and backgroundShape() so the + * blur mask lands on the same boxes — the default single-rectangle mask would cover the gaps + * between slots, which this HUD leaves empty. + */ + private float slotOffset(int index) { + float spacing = mod.spacing.getValue().floatValue(); + // The step was previously left unscaled, so enlarging the HUD made the slots overlap. + return (index * (spacing + 18) - spacing) * scale; + } + + @Override + public void backgroundShape(ShapeSink sink, float originX, float originY) { + boolean horizontal = ArmorDisplay.mode.getValue() == 0; + for (int i = 0; i < SLOTS; i++) { + float offset = slotOffset(i); + sink.rect(horizontal ? originX + offset : originX, + horizontal ? originY : originY + offset, + SLOT_SIZE, SLOT_SIZE); + } + } + @Override public void draw(float x, float y) { super.draw(x, y); List armorInventory = Arrays.asList(mc.thePlayer.inventory.armorInventory); + boolean horizontal = ArmorDisplay.mode.getValue() == 0; for (int i = 0; i < armorInventory.size(); i++) { - ItemStack itemStack = armorInventory.get(i); - int x1 = (int) (x + i * (mod.spacing.getValue().intValue() + 18)) - mod.spacing.getValue().intValue(); - int y1 = (int) y; - - switch (ArmorDisplay.mode.getValue()) { - case 0: - itemStack = armorInventory.get(armorInventory.size() - 1 - i); - break; - case 1: - case 2: - itemStack = armorInventory.get(armorInventory.size() - 1 - i); - x1 = (int) x; - y1 = (int) (y + i * (mod.spacing.getValue().intValue() + 18)) - mod.spacing.getValue().intValue(); - break; - } + ItemStack itemStack = armorInventory.get(armorInventory.size() - 1 - i); + float offset = slotOffset(i); + int x1 = (int) (horizontal ? x + offset : x); + int y1 = (int) (horizontal ? y : y + offset); - drawRect(x1, y1, 16f, 16f, mod.backgroundColor.getColor()); + drawRect(x1, y1, SLOT_SIZE, SLOT_SIZE, mod.backgroundColor.getColor()); if (itemStack == null) continue; GlStateManager.disableCull(); @@ -87,17 +114,6 @@ public void draw(float x, float y) { } } - switch (ArmorDisplay.mode.getValue()) { - case 0: - width = 70f + mod.spacing.getValue().intValue(); - height = 18f; - break; - case 1: - case 2: - width = 70f; - height = 4 + mod.spacing.getValue().intValue() + armorInventory.size() * 16; - break; - } } } diff --git a/src/main/java/top/fpsmaster/ui/custom/impl/CPSDisplayComponent.java b/src/main/java/top/fpsmaster/ui/custom/impl/CPSDisplayComponent.java index f9f45045..ab543ac1 100644 --- a/src/main/java/top/fpsmaster/ui/custom/impl/CPSDisplayComponent.java +++ b/src/main/java/top/fpsmaster/ui/custom/impl/CPSDisplayComponent.java @@ -1,10 +1,10 @@ package top.fpsmaster.ui.custom.impl; -import top.fpsmaster.features.impl.interfaces.CPSDisplay; -import top.fpsmaster.ui.custom.Component; import net.minecraft.util.EnumChatFormatting; +import top.fpsmaster.features.impl.interfaces.CPSDisplay; +import top.fpsmaster.ui.custom.TextComponent; -public class CPSDisplayComponent extends Component { +public class CPSDisplayComponent extends TextComponent { public CPSDisplayComponent() { super(CPSDisplay.class); @@ -14,21 +14,21 @@ public CPSDisplayComponent() { } @Override - public void draw(float x, float y) { - super.draw(x, y); - String text = String.format("CPS: %d%s | %s%d", + protected String text() { + return String.format("CPS: %d%s | %s%d", CPSDisplay.lcps, EnumChatFormatting.GRAY, EnumChatFormatting.RESET, CPSDisplay.rcps); + } - width = getStringWidth(16, text) + 4; - height = 14f; + @Override + protected int fontSize() { + return 16; + } - drawRect(x - 2, y, width, height, mod.backgroundColor.getColor()); - drawString(16, text, x, y + 2, CPSDisplay.textColor.getRGB()); + @Override + protected int textColor() { + return CPSDisplay.textColor.getRGB(); } } - - - diff --git a/src/main/java/top/fpsmaster/ui/custom/impl/ClockDisplayComponent.java b/src/main/java/top/fpsmaster/ui/custom/impl/ClockDisplayComponent.java index 468f1c4b..ba2681ff 100644 --- a/src/main/java/top/fpsmaster/ui/custom/impl/ClockDisplayComponent.java +++ b/src/main/java/top/fpsmaster/ui/custom/impl/ClockDisplayComponent.java @@ -1,13 +1,13 @@ package top.fpsmaster.ui.custom.impl; -import top.fpsmaster.FPSMaster; import top.fpsmaster.features.impl.interfaces.ClockDisplay; -import top.fpsmaster.ui.custom.Component; +import top.fpsmaster.ui.custom.TextComponent; import java.text.SimpleDateFormat; import java.util.Date; -public class ClockDisplayComponent extends Component { +public class ClockDisplayComponent extends TextComponent { + public ClockDisplayComponent() { super(ClockDisplay.class); x = 0.60f; @@ -16,10 +16,8 @@ public ClockDisplayComponent() { } @Override - public void draw(float x, float y) { - super.draw(x, y); + protected String text() { ClockDisplay module = getModule(); - String pattern = module.hour24Mode.getValue() ? "HH:mm" : "hh:mm"; if (module.showSeconds.getValue()) { pattern += ":ss"; @@ -27,30 +25,38 @@ public void draw(float x, float y) { if (!module.hour24Mode.getValue()) { pattern += " a"; } + String label = resolveLabel(module.label.getValue(), "clockdisplay.defaultlabel", "Time: "); + return label + new SimpleDateFormat(pattern).format(new Date()); + } - String timeStr = new SimpleDateFormat(pattern).format(new Date()); - String text = getLabel(module) + timeStr; + @Override + protected int fontSize() { + return 16; + } - width = getStringWidth(16, text) + 8; - height = 16f; + @Override + protected int textColor() { + return getModule().textColor.getRGB(); + } - drawRect(x - 2, y, width, height, mod.backgroundColor.getColor()); - drawString(16, text, x + 2, y + 3, module.textColor.getRGB()); + @Override + protected float horizontalPadding() { + return 8f; } - private String getLabel(ClockDisplay module) { - String label = module.label.getValue(); - if (label == null || label.trim().isEmpty()) { - label = FPSMaster.i18n.get("clockdisplay.defaultlabel"); - if ("clockdisplay.defaultlabel".equals(label)) { - label = "Time: "; - } - module.label.setValue(label); - } - if (!label.endsWith(":") && !label.endsWith(":") && !label.endsWith(" ")) { - label += ": "; - } - return label; + @Override + protected float boxHeight() { + return 16f; + } + + @Override + protected float textOffsetX() { + return 2f; + } + + @Override + protected float textOffsetY() { + return 3f; } private ClockDisplay getModule() { diff --git a/src/main/java/top/fpsmaster/ui/custom/impl/ComboDisplayComponent.java b/src/main/java/top/fpsmaster/ui/custom/impl/ComboDisplayComponent.java index 319adb4a..0e2f385d 100644 --- a/src/main/java/top/fpsmaster/ui/custom/impl/ComboDisplayComponent.java +++ b/src/main/java/top/fpsmaster/ui/custom/impl/ComboDisplayComponent.java @@ -1,9 +1,9 @@ package top.fpsmaster.ui.custom.impl; import top.fpsmaster.features.impl.interfaces.ComboDisplay; -import top.fpsmaster.ui.custom.Component; +import top.fpsmaster.ui.custom.TextComponent; -public class ComboDisplayComponent extends Component { +public class ComboDisplayComponent extends TextComponent { public ComboDisplayComponent() { super(ComboDisplay.class); @@ -11,21 +11,27 @@ public ComboDisplayComponent() { } @Override - public void draw(float x, float y) { - super.draw(x, y); - String text = "Combo: " + ComboDisplay.combo; - if (ComboDisplay.combo == 0) { - text = "No Combo"; - } - - width = getStringWidth(16, text) + 4; - height = 16; - - drawRect(x - 2, y, width, height, mod.backgroundColor.getColor()); - drawString(16, text, x, y + 4, ComboDisplay.textColor.getRGB()); + protected String text() { + return ComboDisplay.combo == 0 ? "No Combo" : "Combo: " + ComboDisplay.combo; + } + @Override + protected int fontSize() { + return 16; } -} + @Override + protected int textColor() { + return ComboDisplay.textColor.getRGB(); + } + @Override + protected float boxHeight() { + return 16f; + } + @Override + protected float textOffsetY() { + return 4f; + } +} diff --git a/src/main/java/top/fpsmaster/ui/custom/impl/CoordsDisplayComponent.java b/src/main/java/top/fpsmaster/ui/custom/impl/CoordsDisplayComponent.java index f36c72ab..ba8b6a42 100644 --- a/src/main/java/top/fpsmaster/ui/custom/impl/CoordsDisplayComponent.java +++ b/src/main/java/top/fpsmaster/ui/custom/impl/CoordsDisplayComponent.java @@ -3,12 +3,12 @@ import org.jetbrains.annotations.NotNull; import top.fpsmaster.features.impl.interfaces.CoordsDisplay; import top.fpsmaster.features.impl.interfaces.FPSDisplay; -import top.fpsmaster.ui.custom.Component; +import top.fpsmaster.ui.custom.TextComponent; import net.minecraft.util.EnumChatFormatting; import static top.fpsmaster.utils.core.Utility.mc; -public class CoordsDisplayComponent extends Component { +public class CoordsDisplayComponent extends TextComponent { public CoordsDisplayComponent() { super(CoordsDisplay.class); @@ -16,28 +16,31 @@ public CoordsDisplayComponent() { } @Override - public void draw(float x, float y) { - super.draw(x, y); - String s = String.format("X:%d Y:%d Z:%d", - (int) mc.thePlayer.posX, - (int) mc.thePlayer.posY, - (int) mc.thePlayer.posZ); - + protected String text() { + if (mc.thePlayer == null) { + return null; + } if (((CoordsDisplay) mod).limitDisplay.getValue()) { - String yStr = getString(); - - s = String.format("X:%d Y:%d(%s) Z:%d", - (int) mc.thePlayer.posX, - (int) mc.thePlayer.posY, - yStr, + return String.format("X:%d Y:%d(%s) Z:%d", + (int) mc.thePlayer.posX, + (int) mc.thePlayer.posY, + getString(), (int) mc.thePlayer.posZ); } + return String.format("X:%d Y:%d Z:%d", + (int) mc.thePlayer.posX, + (int) mc.thePlayer.posY, + (int) mc.thePlayer.posZ); + } - width = getStringWidth(18, s) + 4; - height = 14f; + @Override + protected int fontSize() { + return 18; + } - drawRect(x - 2, y, width, height, mod.backgroundColor.getColor()); - drawString(18, s, x, y + 2, FPSDisplay.textColor.getRGB()); + @Override + protected int textColor() { + return FPSDisplay.textColor.getRGB(); } private @NotNull String getString() { @@ -55,6 +58,3 @@ public void draw(float x, float y) { return yStr; } } - - - diff --git a/src/main/java/top/fpsmaster/ui/custom/impl/FPSDisplayComponent.java b/src/main/java/top/fpsmaster/ui/custom/impl/FPSDisplayComponent.java index b1140d98..8a25b14e 100644 --- a/src/main/java/top/fpsmaster/ui/custom/impl/FPSDisplayComponent.java +++ b/src/main/java/top/fpsmaster/ui/custom/impl/FPSDisplayComponent.java @@ -2,9 +2,9 @@ import net.minecraft.client.Minecraft; import top.fpsmaster.features.impl.interfaces.FPSDisplay; -import top.fpsmaster.ui.custom.Component; +import top.fpsmaster.ui.custom.TextComponent; -public class FPSDisplayComponent extends Component { +public class FPSDisplayComponent extends TextComponent { public FPSDisplayComponent() { super(FPSDisplay.class); @@ -14,17 +14,17 @@ public FPSDisplayComponent() { } @Override - public void draw(float x, float y) { - super.draw(x, y); - String s = Minecraft.getDebugFPS() + "fps"; - - width = getStringWidth(18, s) + 4; - height = 14f; - - drawRect(x - 2, y, width, height, mod.backgroundColor.getColor()); - drawString(18, s, x, y + 2, FPSDisplay.textColor.getRGB()); + protected String text() { + return Minecraft.getDebugFPS() + "fps"; } -} - + @Override + protected int fontSize() { + return 18; + } + @Override + protected int textColor() { + return FPSDisplay.textColor.getRGB(); + } +} diff --git a/src/main/java/top/fpsmaster/ui/custom/impl/KeystrokesComponent.java b/src/main/java/top/fpsmaster/ui/custom/impl/KeystrokesComponent.java index 40cc1e1a..a080a35c 100644 --- a/src/main/java/top/fpsmaster/ui/custom/impl/KeystrokesComponent.java +++ b/src/main/java/top/fpsmaster/ui/custom/impl/KeystrokesComponent.java @@ -37,40 +37,62 @@ public KeystrokesComponent() { } @Override - public void draw(float x, float y) { - super.draw(x, y); - double dt = animClock.tick(); + public void measure() { + // Key offsets are laid out here rather than in draw() so backgroundShape — which the blur mask + // consults before drawing — sees this frame's positions. + int spacing = mod.spacing.getValue().intValue(); for (Key key : keys) { switch (key.keyCode) { case Keyboard.KEY_W: - key.yOffset = key.defaultYOffset - mod.spacing.getValue().intValue(); + key.yOffset = key.defaultYOffset - spacing; break; case Keyboard.KEY_A: - key.xOffset = key.defaultXOffset - mod.spacing.getValue().intValue(); + key.xOffset = key.defaultXOffset - spacing; break; case Keyboard.KEY_D: - key.xOffset = key.defaultXOffset + mod.spacing.getValue().intValue(); + key.xOffset = key.defaultXOffset + spacing; break; case -1: - key.xOffset = key.defaultXOffset - mod.spacing.getValue().intValue(); - key.yOffset = key.defaultYOffset + mod.spacing.getValue().intValue(); + key.xOffset = key.defaultXOffset - spacing; + key.yOffset = key.defaultYOffset + spacing; break; case -2: - key.xOffset = key.defaultXOffset + mod.spacing.getValue().intValue(); - key.yOffset = key.defaultYOffset + mod.spacing.getValue().intValue(); + key.xOffset = key.defaultXOffset + spacing; + key.yOffset = key.defaultYOffset + spacing; break; case Keyboard.KEY_SPACE: - key.xOffset = key.defaultXOffset - mod.spacing.getValue().intValue(); - key.yOffset = key.defaultYOffset + mod.spacing.getValue().intValue() * 2; + key.xOffset = key.defaultXOffset - spacing; + key.yOffset = key.defaultYOffset + spacing * 2; break; } - key.render(x, y, (float) dt, mod.backgroundColor.getColor(), Keystrokes.pressedColor.getColor()); } - float spacing = mod.spacing.getValue().floatValue(); width = 60f + spacing * 2f; height = (Keystrokes.showSpace.getValue() ? 72f : 60f) + spacing * 2f; } + /** + * Keystrokes paints one panel per key, not a single box, so the default single-rectangle mask + * would blur the gaps between keys as well. + */ + @Override + public void backgroundShape(ShapeSink sink, float originX, float originY) { + for (Key key : keys) { + if (key.isHidden()) { + continue; + } + sink.rect(originX + key.xOffset * scale, originY + key.yOffset * scale, key.renderWidth(), key.baseHeight); + } + } + + @Override + public void draw(float x, float y) { + super.draw(x, y); + double dt = animClock.tick(); + for (Key key : keys) { + key.render(x, y, (float) dt, mod.backgroundColor.getColor(), Keystrokes.pressedColor.getColor()); + } + } + public class Key { private final String name; private final int keyCode; @@ -96,14 +118,26 @@ public Key(String name, int keyCode, int xOffset, int yOffset, float width, floa this.yOffset = defaultYOffset; } + /** The space bar is the one key that can be switched off, so it must be skipped by the mask too. */ + private boolean isHidden() { + return keyCode == Keyboard.KEY_SPACE && !Keystrokes.showSpace.getValue(); + } + + /** Single source of truth for key width, shared by render() and backgroundShape(). */ + private float renderWidth() { + return keyCode == Keyboard.KEY_SPACE + ? baseWidth + mod.spacing.getValue().floatValue() * 2f + : baseWidth; + } + public void render(float x, float y, float speed, Color color, Color color1) { - if (keyCode == Keyboard.KEY_SPACE && !Keystrokes.showSpace.getValue()) { + if (isHidden()) { pressAnims.clear(); lastPressed = false; return; } boolean pressed; - float keyW = baseWidth; + float keyW = renderWidth(); float keyH = baseHeight; float keyX; float keyY; @@ -122,11 +156,6 @@ public void render(float x, float y, float speed, Color color, Color color1) { keyX = x + xOffset * scale; keyY = y + yOffset * scale; } - if (keyCode == Keyboard.KEY_SPACE) { - keyW = baseWidth + spacing * 2f; - keyX = x + xOffset * scale; - } - this.color.base(pressed ? color1 : color); if (pressed && !lastPressed) { diff --git a/src/main/java/top/fpsmaster/ui/custom/impl/PingDisplayComponent.java b/src/main/java/top/fpsmaster/ui/custom/impl/PingDisplayComponent.java index ff77c311..3088c806 100644 --- a/src/main/java/top/fpsmaster/ui/custom/impl/PingDisplayComponent.java +++ b/src/main/java/top/fpsmaster/ui/custom/impl/PingDisplayComponent.java @@ -1,11 +1,11 @@ package top.fpsmaster.ui.custom.impl; -import top.fpsmaster.features.impl.interfaces.PingDisplay; import net.minecraft.client.Minecraft; import net.minecraft.client.network.NetworkPlayerInfo; -import top.fpsmaster.ui.custom.Component; +import top.fpsmaster.features.impl.interfaces.PingDisplay; +import top.fpsmaster.ui.custom.TextComponent; -public class PingDisplayComponent extends Component { +public class PingDisplayComponent extends TextComponent { public PingDisplayComponent() { super(PingDisplay.class); @@ -13,26 +13,22 @@ public PingDisplayComponent() { } @Override - public void draw(float x, float y) { - super.draw(x, y); - - // Get ping of connection + protected String text() { Minecraft mc = Minecraft.getMinecraft(); if (mc.thePlayer == null || mc.getNetHandler() == null) { - return; + return null; } - NetworkPlayerInfo info = mc.getNetHandler().getPlayerInfo(mc.thePlayer.getUniqueID()); - String ping = (info != null ? info.getResponseTime() : 0) + "ms"; - String text = "Ping: " + ping; + return "Ping: " + (info != null ? info.getResponseTime() : 0) + "ms"; + } - width = getStringWidth(16, text) + 4; - height = 14f; + @Override + protected int fontSize() { + return 16; + } - drawRect(x - 2, y, width, height, mod.backgroundColor.getColor()); - drawString(16, text, x, y + 2, PingDisplay.textColor.getRGB()); + @Override + protected int textColor() { + return PingDisplay.textColor.getRGB(); } } - - - diff --git a/src/main/java/top/fpsmaster/ui/custom/impl/PlayTimeComponent.java b/src/main/java/top/fpsmaster/ui/custom/impl/PlayTimeComponent.java index 7ce339df..c0363abc 100644 --- a/src/main/java/top/fpsmaster/ui/custom/impl/PlayTimeComponent.java +++ b/src/main/java/top/fpsmaster/ui/custom/impl/PlayTimeComponent.java @@ -4,11 +4,11 @@ import top.fpsmaster.FPSMaster; import top.fpsmaster.features.impl.interfaces.PlayTime; import top.fpsmaster.modules.statistics.PlayTimeStatistics; -import top.fpsmaster.ui.custom.Component; +import top.fpsmaster.ui.custom.TextComponent; import java.util.Locale; -public class PlayTimeComponent extends Component { +public class PlayTimeComponent extends TextComponent { public PlayTimeComponent() { super(PlayTime.class); x = 0.05f; @@ -17,33 +17,43 @@ public PlayTimeComponent() { } @Override - public void draw(float x, float y) { - super.draw(x, y); + protected String text() { PlayTime module = getModule(); - String text = getLabel(module) + formatTime(PlayTimeStatistics.getDisplayMillis(Minecraft.getMinecraft(), module.displayMode.getMode())); + String label = resolveLabel(module.label.getValue(), "playtime.defaultlabel", "Played: "); + return label + formatTime(PlayTimeStatistics.getDisplayMillis(Minecraft.getMinecraft(), module.displayMode.getMode())); + } - width = getStringWidth(16, text) + 8; - height = 16f; + @Override + protected int fontSize() { + return 16; + } - drawRect(x - 2, y, width, height, mod.backgroundColor.getColor()); - drawString(16, text, x + 2, y + 3, module.textColor.getRGB()); + @Override + protected int textColor() { + return getModule().textColor.getRGB(); } - private String getLabel(PlayTime module) { - String label = module.label.getValue(); - if (label == null || label.trim().isEmpty()) { - label = FPSMaster.i18n.get("playtime.defaultlabel"); - if ("playtime.defaultlabel".equals(label)) { - label = "游玩时间:"; - } - module.label.setValue(label); - } - if (!label.endsWith(":") && !label.endsWith(":") && !label.endsWith(" ")) { - label += ":"; - } - return label; + @Override + protected float horizontalPadding() { + return 8f; } + @Override + protected float boxHeight() { + return 16f; + } + + @Override + protected float textOffsetX() { + return 2f; + } + + @Override + protected float textOffsetY() { + return 3f; + } + + private String formatTime(long millis) { long totalSeconds = Math.max(0L, millis / 1000L); if (totalSeconds < 60L) { diff --git a/src/main/java/top/fpsmaster/ui/custom/impl/ReachDisplayComponent.java b/src/main/java/top/fpsmaster/ui/custom/impl/ReachDisplayComponent.java index 8dc279f7..8de47468 100644 --- a/src/main/java/top/fpsmaster/ui/custom/impl/ReachDisplayComponent.java +++ b/src/main/java/top/fpsmaster/ui/custom/impl/ReachDisplayComponent.java @@ -1,9 +1,9 @@ package top.fpsmaster.ui.custom.impl; import top.fpsmaster.features.impl.interfaces.ReachDisplay; -import top.fpsmaster.ui.custom.Component; +import top.fpsmaster.ui.custom.TextComponent; -public class ReachDisplayComponent extends Component { +public class ReachDisplayComponent extends TextComponent { public ReachDisplayComponent() { super(ReachDisplay.class); @@ -11,15 +11,17 @@ public ReachDisplayComponent() { } @Override - public void draw(float x, float y) { - super.draw(x, y); - String s = ReachDisplay.reach + " b"; - width = getStringWidth(18, s) + 4; - height = 14f; - drawRect(x - 2, y, width, height, mod.backgroundColor.getColor()); - drawString(18, s, x, y + 2, ReachDisplay.textColor.getRGB()); + protected String text() { + return ReachDisplay.reach + " b"; } -} - + @Override + protected int fontSize() { + return 18; + } + @Override + protected int textColor() { + return ReachDisplay.textColor.getRGB(); + } +} diff --git a/src/main/java/top/fpsmaster/ui/custom/impl/ServerAddressDisplayComponent.java b/src/main/java/top/fpsmaster/ui/custom/impl/ServerAddressDisplayComponent.java index 93e28cdb..823ca0fe 100644 --- a/src/main/java/top/fpsmaster/ui/custom/impl/ServerAddressDisplayComponent.java +++ b/src/main/java/top/fpsmaster/ui/custom/impl/ServerAddressDisplayComponent.java @@ -4,9 +4,9 @@ import net.minecraft.client.multiplayer.ServerData; import top.fpsmaster.FPSMaster; import top.fpsmaster.features.impl.interfaces.ServerAddressDisplay; -import top.fpsmaster.ui.custom.Component; +import top.fpsmaster.ui.custom.TextComponent; -public class ServerAddressDisplayComponent extends Component { +public class ServerAddressDisplayComponent extends TextComponent { public ServerAddressDisplayComponent() { super(ServerAddressDisplay.class); x = 0.02f; @@ -15,21 +15,43 @@ public ServerAddressDisplayComponent() { } @Override - public void draw(float x, float y) { - super.draw(x, y); - ServerAddressDisplay module = getModule(); - + protected String text() { String address = getServerAddress(); if (address == null) { - return; + return null; } + ServerAddressDisplay module = getModule(); + return resolveLabel(module.label.getValue(), "serveraddressdisplay.defaultlabel", "Server: ") + address; + } + + @Override + protected int fontSize() { + return 16; + } + + @Override + protected int textColor() { + return getModule().textColor.getRGB(); + } + + @Override + protected float horizontalPadding() { + return 8f; + } - String text = getLabel(module) + address; - width = getStringWidth(16, text) + 8; - height = 16f; + @Override + protected float boxHeight() { + return 16f; + } - drawRect(x - 2, y, width, height, mod.backgroundColor.getColor()); - drawString(16, text, x + 2, y + 3, module.textColor.getRGB()); + @Override + protected float textOffsetX() { + return 2f; + } + + @Override + protected float textOffsetY() { + return 3f; } @Override @@ -53,20 +75,6 @@ private String getServerAddress() { return null; } - private String getLabel(ServerAddressDisplay module) { - String label = module.label.getValue(); - if (label == null || label.trim().isEmpty()) { - label = FPSMaster.i18n.get("serveraddressdisplay.defaultlabel"); - if ("serveraddressdisplay.defaultlabel".equals(label)) { - label = "Server: "; - } - module.label.setValue(label); - } - if (!label.endsWith(":") && !label.endsWith(":") && !label.endsWith(" ")) { - label += ": "; - } - return label; - } private ServerAddressDisplay getModule() { return (ServerAddressDisplay) mod;