Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/main/java/top/fpsmaster/features/GlobalListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
64 changes: 57 additions & 7 deletions src/main/java/top/fpsmaster/ui/custom/Component.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>Historically every component assigned its size inside {@code draw()}, but anchoring
* ({@link #getRealPosition}), hover testing, drag clamping and the blur mask all run <em>before</em>
* {@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.
*
* <p>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.
*
* <p>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.
*
* <p>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() {
Expand Down Expand Up @@ -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) {
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/top/fpsmaster/ui/custom/ComponentsManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
93 changes: 93 additions & 0 deletions src/main/java/top/fpsmaster/ui/custom/TextComponent.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.
*
* <p>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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ItemStack> 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();
Expand Down Expand Up @@ -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;
}
}
}

Expand Down
26 changes: 13 additions & 13 deletions src/main/java/top/fpsmaster/ui/custom/impl/CPSDisplayComponent.java
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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();
}
}



Loading
Loading