Compare commits

19 Commits
Author SHA1 Message Date
froslanz df5d90fb9d Merge remote main: README typo fix 2026-10-22 14:30:10 -04:00
froslanz 6e8746c9d1 show [+]/[-] badges on module toggle notifications
toggling a module now announces "[+] Name" in green for enabled and
"[-] Name" in red for disabled instead of the trailing on/off wording.
visual range notifications keep the shared "{gray} name message" format
2026-10-22 13:52:50 -04:00
froslanz f0398c7011 remove the MinecraftFont toggle and always draw text shadows
the toggle existed to switch back to the vanilla font, but disabling it
left every label without a shadow which read as a bug. the setting,
its visibility hooks and FrosFont's vanilla-mode read are gone; Font
and FontSize now always apply, and every label draws its shadow in both
the custom font and the vanilla fallback path. a stale MinecraftFont key
in old configs is ignored by the loader and disappears on next save.
chat and command output keep the untouched vanilla font without shadows
2026-10-22 13:23:25 -04:00
froslanz 7fc42b34d8 darken colored text segments when rasterizing shadows
the shadow pass only darkened the base color, so any segment with an
explicit color code (the watermark's accent from {global}, the slider
value's section-7 gray) punched through at full brightness, drawing a
colored ghost one pixel off the text. shadow rasters now pass the
original color plus a shadow flag, and every segment is darkened at
flush time with the vanilla rule ((c & 0xFCFCFC) >> 2, alpha kept), so
accent text gets a dark accent shadow and gray gets a dark gray one
2026-10-22 13:19:07 -04:00
froslanz 9b6a55d66c snap dragged hud elements to each other and to a grid
while dragging in the hud editor each axis first attracts to the closest
edge or center of any other enabled hud element within 4 gui units, and
otherwise rounds onto a 4px grid so layouts stay tidy. positions are
re-clamped to the screen after snapping and the snap is computed from the
raw pointer position each frame so there is no feedback jitter while
dragging past an alignment
2026-10-22 12:57:02 -04:00
froslanz 3568996138 center clickgui text by the font's cap band instead of vanilla offsets
button labels, panel titles and the color picker's copy/paste text used
y offsets hand-tuned for vanilla's 9px line box, so the custom font's
14px line box rendered text ~4px too low, poking out below 15px buttons
and over the header accent line. FrosFont now measures the distance from
line top to the middle of the cap band (glyph 'A' visual bounds) and all
labels center on that via FrosFont.visualCenter()/Item.textY(). the
vanilla fallback lands on the exact old offsets, so nothing moves when
fonts are disabled, and the legacy getTextOffset() hack is gone
2026-10-22 12:54:00 -04:00
froslanz ebd0a76f0c tie text shadows to the custom font being active
HUD text, clickgui buttons, panel titles and the hud editor labels all
go through FrosFont's default draw now, which renders a drop shadow
only while the custom font is loaded. disabling fonts (MinecraftFont
toggle, or a missing font file falling back) renders everything flat,
matching the vanilla look, in both the hud and the clickgui
2026-10-22 12:27:52 -04:00
froslanz 6ab84a1051 add GUIScale slider for the client's own hud and clickgui
new 0.5x-2.0x slider in the ClickGui settings scales the clickgui, the
hud editor panel and every hud module about its own anchor. mouse
coordinates are divided back into layout units on entry so dragging,
sliders and hover checks keep working, hud hit boxes and drag clamping
use the scaled extents, and the font rasterizer follows the ui scale so
text stays one texel per screen pixel at any scale factor
2026-10-22 12:26:26 -04:00
froslanz 1f69a3ecb4 render font glyphs at device resolution to fix pixelation
glyphs were rasterized at gui-space size and then magnified by the
gui scale with nearest-neighbour sampling, making every custom font
blocky. rasters are now built at font-size * gui-scale (one texel per
screen pixel) and drawn through a 1/scale pose, so text is 1:1 and
antialiased at any gui scale. measurements convert back to gui units
and a gui scale change re-rasterizes everything
2026-10-22 11:53:39 -04:00
froslanz b682961856 add MinecraftFont setting to switch back to the vanilla font
toggling it on hides Font/FontSize and routes all client ui text back
through Minecraft's font renderer
2026-10-22 11:37:15 -04:00
froslanz 7b4fc41dba default the font to java's SansSerif logical font
resolves to a native generic sans on every os (Arial/Helvetica on
windows/mac, DejaVu/Liberation on linux); other installed system font
families can also be picked by name without dropping a file, files in
fros/fonts/ still take priority
2026-10-22 11:34:30 -04:00
froslanz e453c948cc defer font texture releases by 250ms
evicted glyph textures may still be referenced by the current frame's
queued gui draws, so release them only after the frame has flushed
2026-10-22 11:30:58 -04:00
froslanz 8eaec3cc4f cleaner clickgui panels
- solid panel body instead of see-through gray, 1px dark frame around
  each panel and a drop shadow behind it
- accent color line separating header from body
- text shadows on all gui text (panel titles, buttons, hud editor labels)
2026-10-22 11:28:55 -04:00
froslanz 30ccc6ea5a add custom font support for all client ui
- new FrosFont renderer: loads ttf/otf from fros/fonts/, rasterizes
  text to cached textures, falls back to the vanilla font when unset
- Font/FontSize settings in ClickGui (fontSize only shows when a font
  is set); missing files retry automatically every 2s
- all gui buttons, panels and hud modules now render through it
- handles section codes and styled components (watermark accent color)
2026-10-22 11:22:47 -04:00
froslanz d1cb9c8d3a remove gap between arraylist name and module info
info now sits tight against the name, separated only by a single
space character
2026-10-22 10:52:59 -04:00
froslanz 0df0d9334b fix arraylist shifting when module info appears
names are anchored to name widths only so dynamic info (killaura
target) can no longer move the whole list; info now sits directly on
the opposite side of its name
2026-10-22 10:14:36 -04:00
froslanz 7c44258b9b add VisualRange enter/exit notifications to Notifications
uses the radar hud's Range setting so notifications match exactly
what the radar shows
2026-10-22 09:51:12 -04:00
froslanz 4ff4902981 fix config loading, add arraylist module info
- fullbright no longer crashes during config load (mc.options is null
  while the game is still starting, which aborted loading every module
  registered after fullbright - including killaura's keybind)
- one broken module no longer aborts loading the rest
- fix "is a directory" IOException on every startup (commandmanager
  had no file name, so it resolved to the config folder itself)
- arraylist: alignment/sort options, module info shown on the opposite
  side of the row, hides hud/disabled modules behind settings
- killaura: show target name as module info
2026-10-22 08:57:57 -04:00
froslanz 1c3e2ff39f add radar hud module 2026-10-22 07:52:26 -04:00
28 changed files with 1001 additions and 78 deletions
@@ -4,6 +4,7 @@ import cc.fros.Fros;
import cc.fros.features.Feature;
import cc.fros.features.gui.items.buttons.ModuleButton;
import cc.fros.features.modules.Module;
import cc.fros.features.modules.client.ClickGuiModule;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.screens.Screen;
@@ -84,27 +85,39 @@ public class FrosGui extends Screen {
}
context.fill(0, 0, context.guiWidth(), context.guiHeight(), new Color(0, 0, 0, 120).hashCode());
this.widgets.forEach(components -> components.drawScreen(context, mouseX, mouseY, delta));
// scale the whole clickgui; widgets work in layout units so mouse
// coordinates are divided back down before they reach them
float scale = ClickGuiModule.scale();
int layoutX = (int) (mouseX / scale);
int layoutY = (int) (mouseY / scale);
context.pose().pushMatrix();
context.pose().scale(scale, scale);
this.widgets.forEach(components -> components.drawScreen(context, layoutX, layoutY, delta));
context.pose().popMatrix();
}
@Override
public boolean mouseClicked(MouseButtonEvent click, boolean doubled) {
this.widgets.forEach(components -> components.mouseClicked((int) click.x(), (int) click.y(), click.button()));
float scale = ClickGuiModule.scale();
this.widgets.forEach(components -> components.mouseClicked((int) (click.x() / scale), (int) (click.y() / scale), click.button()));
return super.mouseClicked(click, doubled);
}
@Override
public boolean mouseReleased(MouseButtonEvent click) {
this.widgets.forEach(components -> components.mouseReleased((int) click.x(), (int) click.y(), click.button()));
float scale = ClickGuiModule.scale();
this.widgets.forEach(components -> components.mouseReleased((int) (click.x() / scale), (int) (click.y() / scale), click.button()));
return super.mouseReleased(click);
}
@Override
public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) {
int step = Math.max(1, Math.round((float) (10 / ClickGuiModule.scale())));
if (verticalAmount < 0) {
this.widgets.forEach(component -> component.setY(component.getY() - 10));
this.widgets.forEach(component -> component.setY(component.getY() - step));
} else if (verticalAmount > 0) {
this.widgets.forEach(component -> component.setY(component.getY() + 10));
this.widgets.forEach(component -> component.setY(component.getY() + step));
}
return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount);
}
@@ -133,10 +146,6 @@ public class FrosGui extends Screen {
return this.widgets;
}
public int getTextOffset() {
return -6;
}
public static Color getColorClipboard() {
return colorClipboard;
}
@@ -4,6 +4,7 @@ import cc.fros.Fros;
import cc.fros.features.Feature;
import cc.fros.features.gui.items.buttons.ModuleButton;
import cc.fros.features.modules.Module;
import cc.fros.features.modules.client.ClickGuiModule;
import cc.fros.features.modules.client.HudModule;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.screens.Screen;
@@ -40,27 +41,37 @@ public class HudEditorScreen extends Screen {
@Override
public void render(GuiGraphics context, int mouseX, int mouseY, float delta) {
anyHover = false;
this.components.forEach(component -> component.drawScreen(context, mouseX, mouseY, delta));
// same ui scale treatment as the clickgui
float scale = ClickGuiModule.scale();
int layoutX = (int) (mouseX / scale);
int layoutY = (int) (mouseY / scale);
context.pose().pushMatrix();
context.pose().scale(scale, scale);
this.components.forEach(component -> component.drawScreen(context, layoutX, layoutY, delta));
context.pose().popMatrix();
}
@Override
public boolean mouseClicked(MouseButtonEvent click, boolean doubled) {
this.components.forEach(component -> component.mouseClicked((int) click.x(), (int) click.y(), click.button()));
float scale = ClickGuiModule.scale();
this.components.forEach(component -> component.mouseClicked((int) (click.x() / scale), (int) (click.y() / scale), click.button()));
return super.mouseClicked(click, doubled);
}
@Override
public boolean mouseReleased(MouseButtonEvent click) {
this.components.forEach(component -> component.mouseReleased((int) click.x(), (int) click.y(), click.button()));
float scale = ClickGuiModule.scale();
this.components.forEach(component -> component.mouseReleased((int) (click.x() / scale), (int) (click.y() / scale), click.button()));
return super.mouseReleased(click);
}
@Override
public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) {
int step = Math.max(1, Math.round((float) (10 / ClickGuiModule.scale())));
if (verticalAmount < 0) {
this.components.forEach(component -> component.setY(component.getY() - 10));
this.components.forEach(component -> component.setY(component.getY() - step));
} else if (verticalAmount > 0) {
this.components.forEach(component -> component.setY(component.getY() + 10));
this.components.forEach(component -> component.setY(component.getY() + step));
}
return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount);
}
+28 -5
View File
@@ -1,5 +1,8 @@
package cc.fros.features.gui;
import cc.fros.util.render.FrosFont;
import cc.fros.Fros;
import cc.fros.features.Feature;
import cc.fros.features.gui.items.Item;
import cc.fros.features.gui.items.buttons.Button;
@@ -50,12 +53,32 @@ public class Widget
this.context = context;
this.drag(mouseX, mouseY);
float totalItemHeight = this.open ? this.getTotalItemHeight() - 2.0f : 0.0f;
int color = ClickGuiModule.getInstance().topColor.getValue().getRGB();
context.fill(this.x, this.y - 1, this.x + this.width, this.y + this.height - 6, ClickGuiModule.getInstance().rainbow.getValue() ? ColorUtil.rainbow(ClickGuiModule.getInstance().rainbowHue.getValue()).getRGB() : color);
boolean rainbow = ClickGuiModule.getInstance().rainbow.getValue();
int headerColor = rainbow
? ColorUtil.rainbow(ClickGuiModule.getInstance().rainbowHue.getValue()).getRGB()
: ClickGuiModule.getInstance().topColor.getValue().getRGB();
int accent = rainbow
? ColorUtil.rainbow(ClickGuiModule.getInstance().rainbowHue.getValue()).getRGB()
: Fros.colorManager.getColorAsInt();
int headerTop = this.y - 1;
int headerBottom = this.y + this.height - 6;
int panelBottom = this.open ? Math.round(this.y + this.height + totalItemHeight) : headerBottom;
// drop shadow behind the whole panel
context.fill(this.x + 2, headerTop + 2, this.x + this.width + 2, panelBottom + 2, 0x50000000);
context.fill(this.x, headerTop, this.x + this.width, headerBottom, headerColor);
if (this.open) {
RenderUtil.rect(context, this.x, (float) this.y + 12.5f, this.x + this.width, (float) (this.y + this.height) + totalItemHeight, 0x77000000);
RenderUtil.rect(context, this.x, headerBottom, this.x + this.width, panelBottom, 0xE6141414);
}
drawString(this.getName(), (float) this.x + 3.0f, (float) this.y - 4.0f - (float) FrosGui.getClickGui().getTextOffset(), -1);
// frame the panel and separate the header with an accent line
RenderUtil.rect(context, this.x, headerTop, this.x + this.width, panelBottom, 0x99000000, 1.0f);
context.fill(this.x, headerBottom - 1, this.x + this.width, headerBottom, accent);
drawString(this.getName(), (float) this.x + 3.0f,
(headerTop + headerBottom) / 2f - FrosFont.visualCenter(), -1);
ScissorUtil.enable(context, x, 0, x + width, mc.getWindow().getGuiScaledHeight());
if (this.open) {
@@ -193,6 +216,6 @@ public class Widget
}
protected void drawString(String text, double x, double y, int color) {
context.drawString(mc.font, text, (int) x, (int) y, color);
FrosFont.draw(context, text, (int) x, (int) y, color);
}
}
@@ -1,5 +1,7 @@
package cc.fros.features.gui.items;
import cc.fros.util.render.FrosFont;
import cc.fros.features.Feature;
import net.minecraft.client.gui.GuiGraphics;
@@ -78,7 +80,12 @@ public class Item
}
protected void drawString(GuiGraphics context, String text, double x, double y, int color) {
context.drawString(mc.font, text, (int) x, (int) y, color);
FrosFont.draw(context, text, (int) x, (int) y, color);
}
/** y coordinate that visually centers text inside this item */
protected float textY() {
return this.y + this.height / 2f - FrosFont.visualCenter();
}
public boolean isHovering(int mouseX, int mouseY) {
@@ -1,7 +1,6 @@
package cc.fros.features.gui.items.buttons;
import cc.fros.Fros;
import cc.fros.features.gui.FrosGui;
import cc.fros.features.modules.client.ClickGuiModule;
import cc.fros.features.settings.Bind;
import cc.fros.features.settings.Setting;
@@ -29,10 +28,10 @@ public class BindButton
int color = ClickGuiModule.getInstance().color.getValue().getRGB();
RenderUtil.rect(context, this.x, this.y, this.x + (float) this.width + 7.4f, this.y + (float) this.height - 0.5f, this.getState() ? (!this.isHovering(mouseX, mouseY) ? 0x11555555 : -2007673515) : (!this.isHovering(mouseX, mouseY) ? Fros.colorManager.getColorWithAlpha(y, ClickGuiModule.getInstance().color.getValue().getAlpha()) : Fros.colorManager.getColorWithAlpha(y, ClickGuiModule.getInstance().topColor.getValue().getAlpha())));
if (this.isListening) {
drawString(context, "Press a Key...", this.x + 2.3f, this.y - 1.7f - (float) FrosGui.getClickGui().getTextOffset(), -1);
drawString(context, "Press a Key...", this.x + 2.3f, textY(), -1);
} else {
String str = KeyboardUtil.getKeyName(setting.getValue());
drawString(context, this.setting.getName() + " " + ChatFormatting.GRAY + str, this.x + 2.3f, this.y - 1.7f - (float) FrosGui.getClickGui().getTextOffset(), this.getState() ? -1 : -5592406);
drawString(context, this.setting.getName() + " " + ChatFormatting.GRAY + str, this.x + 2.3f, textY(), this.getState() ? -1 : -5592406);
}
}
@@ -2,7 +2,6 @@ package cc.fros.features.gui.items.buttons;
import cc.fros.Fros;
import cc.fros.features.gui.FrosGui;
import cc.fros.features.modules.client.ClickGuiModule;
import cc.fros.features.settings.Setting;
import cc.fros.util.render.RenderUtil;
@@ -23,7 +22,7 @@ public class BooleanButton
@Override
public void drawScreen(GuiGraphics context, int mouseX, int mouseY, float partialTicks) {
RenderUtil.rect(context, this.x, this.y, this.x + (float) this.width + 7.4f, this.y + (float) this.height - 0.5f, this.getState() ? (!this.isHovering(mouseX, mouseY) ? Fros.colorManager.getColorWithAlpha(y, ClickGuiModule.getInstance().color.getValue().getAlpha()) : Fros.colorManager.getColorWithAlpha(y, ClickGuiModule.getInstance().topColor.getValue().getAlpha())) : (!this.isHovering(mouseX, mouseY) ? 0x11555555 : -2007673515));
drawString(context, this.getName(), this.x + 2.3f, this.y - 1.7f - (float) FrosGui.getClickGui().getTextOffset(), this.getState() ? -1 : -5592406);
drawString(context, this.getName(), this.x + 2.3f, textY(), this.getState() ? -1 : -5592406);
}
@Override
@@ -22,7 +22,7 @@ public class Button
@Override
public void drawScreen(GuiGraphics context, int mouseX, int mouseY, float partialTicks) {
RenderUtil.rect(context, this.x, this.y, this.x + (float) this.width, this.y + (float) this.height - 0.5f, this.getState() ? (!this.isHovering(mouseX, mouseY) ? Fros.colorManager.getColorWithAlpha(y, ClickGuiModule.getInstance().color.getValue().getAlpha()) : Fros.colorManager.getColorWithAlpha(y, ClickGuiModule.getInstance().topColor.getValue().getAlpha())) : (!this.isHovering(mouseX, mouseY) ? 0x11555555 : -2007673515));
drawString(context, this.getName(), this.x + 2.3f, this.y - 2.0f - (float) FrosGui.getClickGui().getTextOffset(), this.getState() ? -1 : -5592406);
drawString(context, this.getName(), this.x + 2.3f, textY(), this.getState() ? -1 : -5592406);
}
@Override
@@ -1,5 +1,7 @@
package cc.fros.features.gui.items.buttons;
import cc.fros.util.render.FrosFont;
import cc.fros.Fros;
import cc.fros.features.gui.FrosGui;
import cc.fros.features.gui.Widget;
@@ -42,7 +44,7 @@ public class ColorButton extends Button {
this.x + (float) this.width + 5.0f, this.y + (float) this.height - 2.5f,
new Color(currentColor.getRGB(), false).getRGB());
drawString(context, this.getName(), this.x + 2.3f, this.y - 1.7f - (float) FrosGui.getClickGui().getTextOffset(), -1);
drawString(context, this.getName(), this.x + 2.3f, textY(), -1);
if (open) {
float yOffset = this.height + 1;
@@ -119,12 +121,12 @@ public class ColorButton extends Button {
int buttonWidth = availableWidth / 2;
RenderUtil.rect(context, pickerX, this.y + yOffset, pickerX + buttonWidth, this.y + yOffset + 14,
hoveringCopy ? Fros.colorManager.getColorWithAlpha(y, ClickGuiModule.getInstance().topColor.getValue().getAlpha()) : 0x11555555);
drawString(context, "Copy", pickerX + buttonWidth / 2.0 - mc.font.width("Copy") / 2.0, this.y + yOffset + 3, -1);
drawString(context, "Copy", pickerX + buttonWidth / 2.0 - FrosFont.width("Copy") / 2.0, this.y + yOffset + 7f - FrosFont.visualCenter(), -1);
hoveringCopy = isHoveringArea(mouseX, mouseY, pickerX, this.y + yOffset, pickerX + buttonWidth, this.y + yOffset + 14);
RenderUtil.rect(context, pickerX + buttonWidth + 1, this.y + yOffset, pickerX + buttonWidth * 2 + 1, this.y + yOffset + 14,
hoveringPaste ? Fros.colorManager.getColorWithAlpha(y, ClickGuiModule.getInstance().topColor.getValue().getAlpha()) : 0x11555555);
drawString(context, "Paste", pickerX + buttonWidth + buttonWidth / 2.0 - mc.font.width("Paste") / 2.0 + 1, this.y + yOffset + 3, -1);
drawString(context, "Paste", pickerX + buttonWidth + buttonWidth / 2.0 - FrosFont.width("Paste") / 2.0 + 1, this.y + yOffset + 7f - FrosFont.visualCenter(), -1);
hoveringPaste = isHoveringArea(mouseX, mouseY, pickerX + buttonWidth + 1, this.y + yOffset, pickerX + buttonWidth * 2 + 1, this.y + yOffset + 14);
}
}
@@ -2,7 +2,6 @@ package cc.fros.features.gui.items.buttons;
import cc.fros.Fros;
import cc.fros.features.gui.FrosGui;
import cc.fros.features.modules.client.ClickGuiModule;
import cc.fros.features.settings.Setting;
import cc.fros.util.render.RenderUtil;
@@ -24,7 +23,7 @@ public class EnumButton
@Override
public void drawScreen(GuiGraphics context, int mouseX, int mouseY, float partialTicks) {
RenderUtil.rect(context, this.x, this.y, this.x + (float) this.width + 7.4f, this.y + (float) this.height - 0.5f, this.getState() ? (!this.isHovering(mouseX, mouseY) ? Fros.colorManager.getColorWithAlpha(y, ClickGuiModule.getInstance().color.getValue().getAlpha()) : Fros.colorManager.getColorWithAlpha(y, ClickGuiModule.getInstance().topColor.getValue().getAlpha())) : (!this.isHovering(mouseX, mouseY) ? 0x11555555 : -2007673515));
drawString(context, this.setting.getName() + " " + ChatFormatting.GRAY + (this.setting.currentEnumName().equalsIgnoreCase("ABC") ? "ABC" : this.setting.currentEnumName()), this.x + 2.3f, this.y - 1.7f - (float) FrosGui.getClickGui().getTextOffset(), this.getState() ? -1 : -5592406);
drawString(context, this.setting.getName() + " " + ChatFormatting.GRAY + (this.setting.currentEnumName().equalsIgnoreCase("ABC") ? "ABC" : this.setting.currentEnumName()), this.x + 2.3f, textY(), this.getState() ? -1 : -5592406);
}
@Override
@@ -32,7 +32,7 @@ public class Slider
this.dragSetting(mouseX, mouseY);
RenderUtil.rect(context, this.x, this.y, this.x + (float) this.width + 7.4f, this.y + (float) this.height - 0.5f, !(this.dragging || this.isHovering(mouseX, mouseY)) ? 0x11555555 : -2007673515);
RenderUtil.rect(context, this.x, this.y, (this.setting.getValue()).floatValue() <= this.min.floatValue() ? this.x : this.x + ((float) this.width + 7.4f) * this.partialMultiplier(), this.y + (float) this.height - 0.5f, !(this.dragging || this.isHovering(mouseX, mouseY)) ? Fros.colorManager.getColorWithAlpha(y, ClickGuiModule.getInstance().color.getValue().getAlpha()) : Fros.colorManager.getColorWithAlpha(y, ClickGuiModule.getInstance().topColor.getValue().getAlpha()));
drawString(context, this.getName() + " " + ChatFormatting.GRAY + (this.setting.getValue() instanceof Float ? this.setting.getValue() : Double.valueOf((this.setting.getValue()).doubleValue())), this.x + 2.3f, this.y - 1.7f - (float) FrosGui.getClickGui().getTextOffset(), -1);
drawString(context, this.getName() + " " + ChatFormatting.GRAY + (this.setting.getValue() instanceof Float ? this.setting.getValue() : Double.valueOf((this.setting.getValue()).doubleValue())), this.x + 2.3f, textY(), -1);
}
@Override
@@ -1,7 +1,6 @@
package cc.fros.features.gui.items.buttons;
import cc.fros.Fros;
import cc.fros.features.gui.FrosGui;
import cc.fros.features.modules.client.ClickGuiModule;
import cc.fros.features.settings.Setting;
import cc.fros.util.models.Timer;
@@ -38,9 +37,9 @@ public class StringButton
public void drawScreen(GuiGraphics context, int mouseX, int mouseY, float partialTicks) {
RenderUtil.rect(context, this.x, this.y, this.x + (float) this.width + 7.4f, this.y + (float) this.height - 0.5f, this.getState() ? (!this.isHovering(mouseX, mouseY) ? Fros.colorManager.getColorWithAlpha(y, ClickGuiModule.getInstance().color.getValue().getAlpha()) : Fros.colorManager.getColorWithAlpha(y, ClickGuiModule.getInstance().topColor.getValue().getAlpha())) : (!this.isHovering(mouseX, mouseY) ? 0x11555555 : -2007673515));
if (this.isListening) {
drawString(context, this.currentString.string() + getIdleSign(), this.x + 2.3f, this.y - 1.7f - (float) FrosGui.getClickGui().getTextOffset(), this.getState() ? -1 : -5592406);
drawString(context, this.currentString.string() + getIdleSign(), this.x + 2.3f, textY(), this.getState() ? -1 : -5592406);
} else {
drawString(context, (this.setting.getName().equals("Buttons") ? "Buttons " : (this.setting.getName().equals("Prefix") ? "Prefix " + ChatFormatting.GRAY : "")) + this.setting.getValue(), this.x + 2.3f, this.y - 1.7f - (float) FrosGui.getClickGui().getTextOffset(), this.getState() ? -1 : -5592406);
drawString(context, (this.setting.getName().equals("Buttons") ? "Buttons " : (this.setting.getName().equals("Prefix") ? "Prefix " + ChatFormatting.GRAY : "")) + this.setting.getValue(), this.x + 2.3f, textY(), this.getState() ? -1 : -5592406);
}
}
@@ -16,6 +16,9 @@ import static cc.fros.features.commands.MessageSignatures.GENERAL;
public class ClickGuiModule extends Module {
private static ClickGuiModule INSTANCE;
public final Setting<String> font = str("Font", "SansSerif");
public final Setting<Integer> fontSize = num("FontSize", 10, 6, 24);
public final Setting<Float> guiScale = num("GUIScale", 1.0f, 0.5f, 2.0f);
public final Setting<String> prefix = str("Prefix", ".");
public final Setting<Color> color = color("Color", 0, 0, 255, 180);
public final Setting<Color> topColor = color("TopColor", 0, 0, 150, 240);
@@ -27,6 +30,7 @@ public class ClickGuiModule extends Module {
public ClickGuiModule() {
super("ClickGui", "Opens the ClickGui", Module.Category.CLIENT);
setBind(GLFW.GLFW_KEY_RIGHT_SHIFT);
fontSize.setVisibility(v -> !font.getValue().isEmpty());
rainbowHue.setVisibility(v -> rainbow.getValue());
rainbowBrightness.setVisibility(v -> rainbow.getValue());
rainbowSaturation.setVisibility(v -> rainbow.getValue());
@@ -70,4 +74,9 @@ public class ClickGuiModule extends Module {
public static ClickGuiModule getInstance() {
return INSTANCE;
}
/** ui scale factor applied to the client's own hud and clickgui */
public static float scale() {
return INSTANCE == null ? 1f : Math.max(0.5f, INSTANCE.guiScale.getValue());
}
}
@@ -1,5 +1,7 @@
package cc.fros.features.modules.client;
import cc.fros.util.render.FrosFont;
import cc.fros.Fros;
import cc.fros.event.impl.input.MouseInputEvent;
import cc.fros.event.impl.render.Render2DEvent;
@@ -11,9 +13,16 @@ import cc.fros.util.render.RenderUtil;
import net.minecraft.client.gui.screens.ChatScreen;
import org.joml.Vector2f;
import java.util.List;
public abstract class HudModule extends Module {
public final Setting<Vector2f> pos = vec2f("Position", 0.5f, 0.5f);
/** grid snap lines (gui units) for dragged hud elements */
private static final float SNAP_GRID = 4f;
/** how close a dragged element gets to another's edge/center before it attracts */
private static final float SNAP_RANGE = 4f;
private float dragX, dragY, width, height;
private boolean dragging, button;
@@ -28,19 +37,36 @@ public abstract class HudModule extends Module {
}
public float getY() {
float scaledHeight = getHeight() * ClickGuiModule.scale();
float heightWithChat = mc.getWindow().getGuiScaledHeight() - 14;
float baseY = mc.getWindow().getGuiScaledHeight() * pos.getValue().y();
float combined = baseY + getHeight();
float combined = baseY + scaledHeight;
if (mc.screen instanceof ChatScreen) {
baseY = Math.min(combined, heightWithChat) - getHeight();
baseY = Math.min(combined, heightWithChat) - scaledHeight;
}
return baseY;
}
@Subscribe
public void onRender2DHud(Render2DEvent e) {
render(e);
float scale = ClickGuiModule.scale();
if (scale == 1f) {
render(e);
return;
}
// scale the element about its own position anchor so content,
// editor box and hover label all grow together
float anchorX = getX();
float anchorY = getY();
e.getContext().pose().pushMatrix();
e.getContext().pose().translate(anchorX * (1f - scale), anchorY * (1f - scale));
e.getContext().pose().scale(scale, scale);
try {
render(e);
} finally {
e.getContext().pose().popMatrix();
}
}
@Subscribe
@@ -73,12 +99,16 @@ public abstract class HudModule extends Module {
}
if (dragging) {
float finalX = Math.min(Math.max(getMouseX() - dragX, 0),
mc.getWindow().getGuiScaledWidth() - width);
float finalY = Math.min(Math.max(getMouseY() - dragY, 0),
mc.getWindow().getGuiScaledHeight() - height);
float scale = ClickGuiModule.scale();
float maxX = mc.getWindow().getGuiScaledWidth() - width * scale;
float maxY = mc.getWindow().getGuiScaledHeight() - height * scale;
float rawX = Math.min(Math.max(getMouseX() - dragX, 0), maxX);
float rawY = Math.min(Math.max(getMouseY() - dragY, 0), maxY);
// snap to other elements first, grid otherwise, then re-clamp
float snappedX = Math.min(Math.max(snapAxis(rawX, width * scale, true, scale), 0), maxX);
float snappedY = Math.min(Math.max(snapAxis(rawY, height * scale, false, scale), 0), maxY);
pos.setValue(new Vector2f(finalX, finalY)
pos.setValue(new Vector2f(snappedX, snappedY)
.div(mc.getWindow().getGuiScaledWidth(), mc.getWindow().getGuiScaledHeight()));
}
} else {
@@ -91,13 +121,15 @@ public abstract class HudModule extends Module {
}
if (shouldDrawDescription) {
int textWidth = mc.font.width(getName());
int textHeight = mc.font.lineHeight;
int textWidth = FrosFont.width(getName());
int textHeight = FrosFont.lineHeight();
float scale = ClickGuiModule.scale();
float textX = x + width + 5;
if (textX + textWidth > mc.getWindow().getGuiScaledWidth()) {
// the label is drawn inside the element's scale transform
if (x + (textX + textWidth - x) * scale > mc.getWindow().getGuiScaledWidth()) {
textX = x - 5 - textWidth;
}
e.getContext().drawString(mc.font, getName(),
FrosFont.draw(e.getContext(), getName(),
(int) textX, (int) (y + height / 2f - textHeight / 2f), -1);
HudEditorScreen.getInstance().anyHover = true;
}
@@ -127,9 +159,47 @@ public abstract class HudModule extends Module {
float y = getY();
int mouseX = getMouseX();
int mouseY = getMouseY();
float scale = ClickGuiModule.scale();
return mouseX >= x - 1 && mouseX <= x + width + 1 &&
mouseY >= y - 1 && mouseY <= y + height + 1;
return mouseX >= x - 1 && mouseX <= x + width * scale + 1 &&
mouseY >= y - 1 && mouseY <= y + height * scale + 1;
}
/**
* Drags on to another visible hud element's left/center/right (or
* top/middle/bottom) when within {@link #SNAP_RANGE}; otherwise rounds
* onto the {@link #SNAP_GRID} grid so everything stays tidy.
*/
private float snapAxis(float position, float size, boolean horizontal, float scale) {
float bestDelta = 0f;
boolean aligned = false;
for (HudModule other : hudModules()) {
if (other == this || !other.isEnabled()) continue;
float otherPos = horizontal ? other.getX() : other.getY();
float otherSize = horizontal ? other.getWidth() * scale : other.getHeight() * scale;
float[] mine = {position, position + size / 2f, position + size};
float[] theirs = {otherPos, otherPos + otherSize / 2f, otherPos + otherSize};
for (float mineAnchor : mine) {
for (float theirAnchor : theirs) {
float delta = theirAnchor - mineAnchor;
if (Math.abs(delta) <= SNAP_RANGE && (!aligned || Math.abs(delta) < Math.abs(bestDelta))) {
bestDelta = delta;
aligned = true;
}
}
}
}
if (aligned) return position + bestDelta;
return Math.round(position / SNAP_GRID) * SNAP_GRID;
}
private static List<HudModule> hudModules() {
return Fros.moduleManager.stream()
.filter(module -> module instanceof HudModule)
.map(module -> (HudModule) module)
.toList();
}
public float getWidth() {
@@ -1,18 +1,33 @@
package cc.fros.features.modules.client;
import cc.fros.Fros;
import cc.fros.event.impl.ClientEvent;
import cc.fros.event.system.Subscribe;
import cc.fros.features.modules.Module;
import cc.fros.features.modules.hud.RadarHudModule;
import cc.fros.features.settings.Setting;
import cc.fros.util.TextUtil;
import cc.fros.util.chat.ChatUtil;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.world.entity.player.Player;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import static cc.fros.util.chat.SimpleSignature.from;
public class NotificationsModule extends Module {
private static final String MODULE_FORMAT = "{gray} %s %s%s";
private static final String TOGGLE_FORMAT = "%s%s {gray}%s";
public final Setting<Boolean> moduleToggle = bool("ModuleToggle", true);
public final Setting<Boolean> visualRange = bool("VisualRange", true);
private final Map<UUID, String> inRange = new HashMap<>();
private ClientLevel lastLevel;
private RadarHudModule radar;
private boolean tracked;
public NotificationsModule() {
super("Notifications", "Displays notifications for various client events", Category.CLIENT);
@@ -31,9 +46,62 @@ public class NotificationsModule extends Module {
? module.getDisplayName()
: event.getFeature().getName();
ChatUtil.sendMessage(TextUtil.text(TOGGLE_FORMAT,
moduleState ? "{green}" : "{red}",
moduleState ? "[+]" : "[-]",
name), from(event.getFeature().getName()));
}
@Override
public void onTick() {
if (!visualRange.getValue() || mc.player == null || mc.level == null) {
inRange.clear();
tracked = false;
return;
}
if (mc.level != lastLevel) {
// world change: re-baseline silently instead of notifying everyone present
lastLevel = mc.level;
tracked = false;
inRange.clear();
}
double range = getRadarRange();
Map<UUID, String> now = new HashMap<>();
for (Player player : mc.level.players()) {
if (player == mc.player) continue;
if (mc.player.distanceTo(player) <= range) {
now.put(player.getUUID(), player.getScoreboardName());
}
}
if (tracked) {
for (Map.Entry<UUID, String> entry : now.entrySet()) {
if (inRange.containsKey(entry.getKey())) continue;
notify(entry.getValue(), "{green}", "entered your visual range");
}
for (Map.Entry<UUID, String> entry : inRange.entrySet()) {
if (now.containsKey(entry.getKey())) continue;
notify(entry.getValue(), "{red}", "left your visual range");
}
}
inRange.clear();
inRange.putAll(now);
tracked = true;
}
private void notify(String name, String color, String message) {
ChatUtil.sendMessage(TextUtil.text(MODULE_FORMAT,
name,
moduleState ? "{green}" : "{red}",
moduleState ? "on" : "off"), from(event.getFeature().getName()));
color,
message), from(getName() + ":" + name));
}
private double getRadarRange() {
// stay in sync with what the radar hud actually shows
if (radar == null) radar = Fros.moduleManager.getModuleByClass(RadarHudModule.class);
return radar != null ? radar.range.getValue() : 64.0;
}
}
@@ -19,21 +19,25 @@ public class KillAuraModule extends Module {
private final Setting<Boolean> onlyWeapon = bool("OnlyWeapon", false);
private final Setting<Boolean> sync = bool("MovementSync", false);
private String targetName;
public KillAuraModule() {
super("KillAura", "Attacks nearby players.", Category.COMBAT);
}
@Subscribe public void onPreTick(TickEvent.Pre event) {
Player target = findPlayerTarget();
this.targetName = target == null ? null : target.getScoreboardName();
if (target == null)
return;
if (isOnCooldown())
return;
if (onlyWeapon.getValue() && !isHoldingWeapon())
return;
Player target = findPlayerTarget();
if (target == null)
return;
Angles angles = Angles.entity(target);
int priority = 0;
@@ -74,4 +78,14 @@ public class KillAuraModule extends Module {
}
return target;
}
@Override
public void onDisable() {
targetName = null;
}
@Override
public String getDisplayInfo() {
return targetName;
}
}
@@ -0,0 +1,120 @@
package cc.fros.features.modules.hud;
import cc.fros.util.render.FrosFont;
import cc.fros.Fros;
import cc.fros.event.impl.render.Render2DEvent;
import cc.fros.features.modules.client.HudModule;
import cc.fros.features.modules.Module;
import cc.fros.features.settings.Setting;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
public class ArrayListHudModule extends HudModule {
private static final int DISABLED_COLOR = 0xFF555555;
private static final int BRACKET_COLOR = 0xFFAAAAAA;
public final Setting<Align> alignment = mode("Alignment", Align.RIGHT);
public final Setting<Sort> sort = mode("Sort", Sort.LENGTH);
public final Setting<Boolean> disabled = bool("Disabled", false);
public final Setting<Boolean> hudModules = bool("HudModules", false);
public ArrayListHudModule() {
super("ArrayList", "Display modules and their state", 100, 10);
}
@Override
protected void render(Render2DEvent e) {
super.render(e);
List<Module> shown = Fros.moduleManager.getModules().stream()
.filter(module -> !module.hidden)
.filter(module -> module.getCategory() != Module.Category.HUD || hudModules.getValue())
.filter(module -> module.isEnabled() || disabled.getValue())
.sorted(comparator())
.toList();
if (shown.isEmpty()) return;
float x = getX();
float y = getY();
int lineHeight = FrosFont.lineHeight();
int color = Fros.colorManager.getColorAsInt();
boolean right = alignment.getValue() == Align.RIGHT;
int nameBlock = 0;
int content = 0;
for (Module module : shown) {
int nameWidth = FrosFont.width(module.getDisplayName());
nameBlock = Math.max(nameBlock, nameWidth);
int rowWidth = nameWidth;
String info = module.getDisplayInfo();
if (info != null) {
rowWidth += FrosFont.width(" ") + infoWidth(info);
}
content = Math.max(content, rowWidth);
}
for (int i = 0; i < shown.size(); i++) {
Module module = shown.get(i);
String name = module.getDisplayName();
String info = module.getDisplayInfo();
boolean on = module.isEnabled();
int rowY = (int) (y + i * lineHeight);
int baseColor = on ? color : DISABLED_COLOR;
int nameWidth = FrosFont.width(name);
// name is anchored to the alignment side using names only, so it
// never moves when dynamic module info appears or changes
int nameX = (int) (right ? x + nameBlock - nameWidth : x);
FrosFont.draw(e.getContext(), name, nameX, rowY, baseColor);
if (info != null) {
// info sits directly on the opposite side of the name,
// separated by a single space instead of a gap
int blockWidth = infoWidth(info);
int space = FrosFont.width(" ");
int infoX = right ? nameX - space - blockWidth : nameX + nameWidth + space;
int bracketColor = on ? BRACKET_COLOR : DISABLED_COLOR;
FrosFont.draw(e.getContext(), "[", infoX, rowY, bracketColor);
infoX += FrosFont.width("[");
FrosFont.draw(e.getContext(), info, infoX, rowY, on ? -1 : DISABLED_COLOR);
infoX += FrosFont.width(info);
FrosFont.draw(e.getContext(), "]", infoX, rowY, bracketColor);
}
}
setWidth(Math.max(content, nameBlock));
setHeight(lineHeight * shown.size());
}
private int infoWidth(String info) {
return FrosFont.width("[") + FrosFont.width(info) + FrosFont.width("]");
}
private Comparator<Module> comparator() {
return switch (sort.getValue()) {
case LENGTH -> Comparator.comparingInt((Module module) ->
module.getDisplayName().length()).reversed();
case ALPHABETICAL -> Comparator.comparing(module ->
module.getDisplayName().toLowerCase(Locale.ROOT));
case NONE -> (first, second) -> 0;
};
}
public enum Align {
LEFT,
RIGHT
}
public enum Sort {
LENGTH,
ALPHABETICAL,
NONE
}
}
@@ -1,5 +1,7 @@
package cc.fros.features.modules.hud;
import cc.fros.util.render.FrosFont;
import cc.fros.event.impl.render.Render2DEvent;
import cc.fros.features.modules.client.HudModule;
import cc.fros.features.settings.Setting;
@@ -33,11 +35,11 @@ public class CoordinatesHudModule extends HudModule {
}
}
e.getContext().drawString(mc.font, coordsStr,
FrosFont.draw(e.getContext(), coordsStr,
(int) getX(), (int) getY(), -1);
setWidth(mc.font.width(coordsStr));
setHeight(mc.font.lineHeight);
setWidth(FrosFont.width(coordsStr));
setHeight(FrosFont.lineHeight());
}
}
@@ -1,5 +1,7 @@
package cc.fros.features.modules.hud;
import cc.fros.util.render.FrosFont;
import cc.fros.event.impl.render.Render2DEvent;
import cc.fros.features.modules.client.HudModule;
import cc.fros.features.settings.Setting;
@@ -18,10 +20,10 @@ public class FpsHudModule extends HudModule {
int fps = mc.getFps();
String fpsStr = label.getValue() ? "FPS: " + fps : String.valueOf(fps);
e.getContext().drawString(mc.font, fpsStr,
FrosFont.draw(e.getContext(), fpsStr,
(int) getX(), (int) getY(), -1);
setWidth(mc.font.width(fpsStr));
setHeight(mc.font.lineHeight);
setWidth(FrosFont.width(fpsStr));
setHeight(FrosFont.lineHeight());
}
}
@@ -1,5 +1,7 @@
package cc.fros.features.modules.hud;
import cc.fros.util.render.FrosFont;
import cc.fros.event.impl.render.Render2DEvent;
import cc.fros.features.modules.client.HudModule;
import cc.fros.features.settings.Setting;
@@ -36,11 +38,11 @@ public class PingHudModule extends HudModule {
}
private void draw(Render2DEvent e, String text, int color) {
e.getContext().drawString(mc.font, text,
FrosFont.draw(e.getContext(), text,
(int) getX(), (int) getY(), color);
setWidth(mc.font.width(text));
setHeight(mc.font.lineHeight);
setWidth(FrosFont.width(text));
setHeight(FrosFont.lineHeight());
}
private static int colorForPing(int ping) {
@@ -0,0 +1,69 @@
package cc.fros.features.modules.hud;
import cc.fros.util.render.FrosFont;
import cc.fros.Fros;
import cc.fros.event.impl.render.Render2DEvent;
import cc.fros.features.modules.client.HudModule;
import cc.fros.features.settings.Setting;
import net.minecraft.world.entity.player.Player;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class RadarHudModule extends HudModule {
public final Setting<Double> range = num("Range", 64.0, 8.0, 256.0);
public final Setting<Integer> maxPlayers = num("MaxPlayers", 8, 1, 32);
public final Setting<Boolean> showDistance = bool("Distance", true);
private record Entry(String name, double distance, boolean friend) {
}
public RadarHudModule() {
super("Radar", "Display players near you", 100, 10);
}
@Override
protected void render(Render2DEvent e) {
super.render(e);
if (nullCheck()) return;
List<Entry> nearby = new ArrayList<>();
for (Player player : mc.level.players()) {
if (player == mc.player) continue;
double distance = mc.player.distanceTo(player);
if (distance > range.getValue()) continue;
nearby.add(new Entry(player.getScoreboardName(), distance,
Fros.friendManager.isFriend(player)));
}
if (nearby.isEmpty()) return;
nearby.sort(Comparator.comparingDouble(Entry::distance));
int limit = Math.min(nearby.size(), maxPlayers.getValue());
nearby = nearby.subList(0, limit);
float x = getX();
float y = getY();
int lineHeight = FrosFont.lineHeight();
int widest = 0;
for (int i = 0; i < nearby.size(); i++) {
Entry entry = nearby.get(i);
String text = showDistance.getValue()
? entry.name() + " " + (int) Math.round(entry.distance()) + "m"
: entry.name();
int color = entry.friend() ? 0xFF55FF55 : -1;
FrosFont.draw(e.getContext(), text, (int) x, (int) (y + i * lineHeight), color);
widest = Math.max(widest, FrosFont.width(text));
}
setWidth(widest);
setHeight(lineHeight * nearby.size());
}
}
@@ -1,5 +1,7 @@
package cc.fros.features.modules.hud;
import cc.fros.util.render.FrosFont;
import cc.fros.event.impl.render.Render2DEvent;
import cc.fros.features.modules.client.HudModule;
import cc.fros.features.settings.Setting;
@@ -24,10 +26,10 @@ public class TimeHudModule extends HudModule {
DateTimeFormatter formatter = twentyFourHour.getValue() ? TWENTY_FOUR_HOUR : TWELVE_HOUR;
String timeStr = LocalTime.now().format(formatter);
e.getContext().drawString(mc.font, timeStr,
FrosFont.draw(e.getContext(), timeStr,
(int) getX(), (int) getY(), -1);
setWidth(mc.font.width(timeStr));
setHeight(mc.font.lineHeight);
setWidth(FrosFont.width(timeStr));
setHeight(FrosFont.lineHeight());
}
}
@@ -1,5 +1,7 @@
package cc.fros.features.modules.hud;
import cc.fros.util.render.FrosFont;
import cc.fros.event.impl.render.Render2DEvent;
import cc.fros.features.modules.client.HudModule;
import cc.fros.features.settings.Setting;
@@ -27,10 +29,10 @@ public class WatermarkHudModule extends HudModule {
}
Component component = TextUtil.text(watermarkString, text.getValue(), BuildConfig.VERSION);
e.getContext().drawString(mc.font, component, (int) getX(), (int) getY(), -1);
FrosFont.draw(e.getContext(), component, (int) getX(), (int) getY(), -1);
setWidth(mc.font.width(component));
setHeight(mc.font.lineHeight);
setWidth(FrosFont.width(component));
setHeight(FrosFont.lineHeight());
}
}
@@ -1,5 +1,7 @@
package cc.fros.features.modules.hud;
import cc.fros.util.render.FrosFont;
import cc.fros.event.impl.render.Render2DEvent;
import cc.fros.features.modules.client.HudModule;
import cc.fros.features.settings.Setting;
@@ -17,10 +19,10 @@ public class WelcomerHudModule extends HudModule {
String welcomeStr = greeting.getValue() + ", " + mc.getUser().getName();
e.getContext().drawString(mc.font, welcomeStr,
FrosFont.draw(e.getContext(), welcomeStr,
(int) getX(), (int) getY(), -1);
setWidth(mc.font.width(welcomeStr));
setHeight(mc.font.lineHeight);
setWidth(FrosFont.width(welcomeStr));
setHeight(FrosFont.lineHeight());
}
}
@@ -14,6 +14,7 @@ public class FullbrightModule extends Module {
private Mode activeMode = Mode.GAMMA;
private double previousGamma;
private boolean gammaCaptured;
private boolean hadNightVision;
public FullbrightModule() {
@@ -52,16 +53,28 @@ public class FullbrightModule extends Module {
private void prepare(Mode newMode) {
if (newMode == Mode.GAMMA) {
previousGamma = mc.options.gamma().get();
gammaCaptured = false;
// during config load mc.options is still null, so this can be a no-op;
// applyGamma() captures lazily on the first real tick instead
captureGamma();
return;
}
hadNightVision = !nullCheck() && mc.player.hasEffect(MobEffects.NIGHT_VISION);
}
private void captureGamma() {
if (gammaCaptured || mc.options == null) return;
previousGamma = mc.options.gamma().get();
gammaCaptured = true;
}
private void restore(Mode oldMode) {
if (oldMode == Mode.GAMMA) {
// bypass OptionInstance#set, which rejects values outside [0.0, 1.0]
mc.options.gamma().value = previousGamma;
if (gammaCaptured && mc.options != null) {
// bypass OptionInstance#set, which rejects values outside [0.0, 1.0]
mc.options.gamma().value = previousGamma;
}
gammaCaptured = false;
return;
}
if (!nullCheck() && !hadNightVision) {
@@ -70,6 +83,8 @@ public class FullbrightModule extends Module {
}
private void applyGamma() {
if (mc.options == null) return;
captureGamma();
mc.options.gamma().value = gamma.getValue();
}
@@ -111,4 +111,9 @@ public class CommandManager extends Feature implements Jsonable {
setCommandPrefix(object.get("prefix").getAsString());
}
}
@Override
public String getFileName() {
return "commands.json";
}
}
@@ -35,7 +35,9 @@ public class ConfigManager {
mkdirs();
for (Jsonable jsonable : jsonables) {
Path path = FROS_PATH.resolve(jsonable.getFileName());
if (!Files.exists(path)) continue;
// isRegularFile, not exists: a Jsonable with no file name resolves to the
// config directory itself, and readString on a directory throws IOException
if (!Files.isRegularFile(path)) continue;
try {
String read = Files.readString(path);
jsonable.fromJson(JsonParser.parseString(read));
@@ -51,6 +53,7 @@ public class ConfigManager {
try {
JsonElement json = jsonable.toJson();
Path path = FROS_PATH.resolve(jsonable.getFileName());
if (Files.isDirectory(path)) continue;
Files.writeString(path, GSON.toJson(json));
} catch (Throwable e) {
LOGGER.error("Failed to write to file", e);
@@ -17,9 +17,11 @@ import cc.fros.features.modules.client.NotificationsModule;
import cc.fros.features.modules.combat.KillAuraModule;
import cc.fros.features.modules.combat.CriticalsModule;
import cc.fros.features.modules.combat.KeyPearlModule;
import cc.fros.features.modules.hud.ArrayListHudModule;
import cc.fros.features.modules.hud.CoordinatesHudModule;
import cc.fros.features.modules.hud.FpsHudModule;
import cc.fros.features.modules.hud.PingHudModule;
import cc.fros.features.modules.hud.RadarHudModule;
import cc.fros.features.modules.hud.TimeHudModule;
import cc.fros.features.modules.hud.WelcomerHudModule;
import cc.fros.features.modules.hud.WatermarkHudModule;
@@ -47,10 +49,12 @@ public class ModuleManager implements Jsonable, Util {
private final List<Module> modules = new ArrayList<>();
public void init() {
register(new ArrayListHudModule());
register(new WatermarkHudModule());
register(new CoordinatesHudModule());
register(new FpsHudModule());
register(new PingHudModule());
register(new RadarHudModule());
register(new WelcomerHudModule());
register(new TimeHudModule());
register(new HudEditorModule());
@@ -163,8 +167,14 @@ public class ModuleManager implements Jsonable, Util {
@Override
public void fromJson(JsonElement element) {
if (element == null || !element.isJsonObject()) return;
for (Module module : getModules()) {
module.fromJson(element.getAsJsonObject().get(module.getName()));
// one broken module must never abort loading the rest
try {
module.fromJson(element.getAsJsonObject().get(module.getName()));
} catch (Throwable throwable) {
LOGGER.error("Failed to load module {}", module.getName(), throwable);
}
}
}
@@ -0,0 +1,480 @@
package cc.fros.util.render;
import cc.fros.features.modules.client.ClickGuiModule;
import com.mojang.blaze3d.platform.NativeImage;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.Style;
import net.minecraft.network.chat.TextColor;
import net.minecraft.resources.Identifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Color;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import static cc.fros.util.traits.Util.mc;
/**
* Renders client UI text with a user supplied ttf/otf font.
* Text is rasterized with AWT and cached as textures; when no font
* is configured everything falls through to the vanilla font.
*/
public final class FrosFont {
private static final Logger LOGGER = LoggerFactory.getLogger("FrosFont");
private static final Path FONT_DIR = FabricLoader.getInstance().getGameDir().resolve("fros").resolve("fonts");
private static final int MAX_CACHE = 400;
private static final int[] PALETTE = {
0x000000, 0x0000AA, 0x00AA00, 0x00AAAA,
0xAA0000, 0xAA00AA, 0xFFAA00, 0xAAAAAA,
0x555555, 0x5555FF, 0x55FF55, 0x55FFFF,
0xFF5555, 0xFF55FF, 0xFFFF55, 0xFFFFFF
};
private static final char[] HEX = "0123456789abcdef".toCharArray();
// java guarantees these logical fonts on every os
private static final Map<String, String> LOGICAL_FONTS = Map.of(
"sansserif", "SansSerif",
"serif", "Serif",
"monospaced", "Monospaced");
private static String[] installedFamilies;
private static final Map<Key, Glyph> GLYPHS = new LinkedHashMap<>(128, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Key, Glyph> eldest) {
if (size() <= MAX_CACHE) return false;
queueRelease(eldest.getValue().id());
return true;
}
};
// textures still referenced by this frame's queued draws are only
// released a moment later, once the frame has been flushed
private static final List<Identifier> PENDING_RELEASE = new ArrayList<>();
private static long lastQueuedAt;
private record Key(int color, String text, boolean shadow) {
}
private record Glyph(Identifier id, int width, int height) {
}
private static java.awt.Font awtFont;
private static FontMetrics metrics;
private static String loadedName;
private static float loadedSize = Float.NaN;
private static float loadedScale = 1;
/** raster pixels from the top of a line box to the middle of the cap band */
private static float visualCenterRaster = 4f;
private static long lastAttempt;
private static int glyphCounter;
private FrosFont() {
}
public static int width(String text) {
if (text == null || text.isEmpty()) return 0;
sync();
if (awtFont == null) return mc.font.width(text);
return toGui(metrics.stringWidth(stripCodes(text)));
}
public static int width(Component component) {
if (component == null) return 0;
sync();
if (awtFont == null) return mc.font.width(component);
return width(toCodeString(component));
}
public static int lineHeight() {
sync();
return awtFont == null ? mc.font.lineHeight : toGui(metrics.getHeight());
}
/** converts a raster pixel measurement to gui units */
private static int toGui(int pixels) {
return (int) Math.ceil(pixels / (double) loadedScale);
}
private static float currentScale() {
try {
// one raster pixel per screen pixel, following both mc's gui scale
// and the client's own ui scale slider
return Math.max(0.5f, (float) (mc.getWindow().getGuiScale() * ClickGuiModule.scale()));
} catch (Exception e) {
return 1f;
}
}
public static void draw(GuiGraphics context, String text, int x, int y, int color) {
draw(context, text, x, y, color, true);
}
public static void draw(GuiGraphics context, String text, int x, int y, int color, boolean shadow) {
if (text == null || text.isEmpty()) return;
sync();
if (awtFont == null) {
context.drawString(mc.font, text, x, y, color, shadow);
return;
}
if (shadow) drawGlyphs(context, text, x + 1, y + 1, color, true);
drawGlyphs(context, text, x, y, color, false);
}
public static void draw(GuiGraphics context, Component component, int x, int y, int color) {
if (component == null) return;
sync();
if (awtFont == null) {
context.drawString(mc.font, component, x, y, color, true);
return;
}
draw(context, toCodeString(component), x, y, color);
}
// --- loading ---
private static void sync() {
ClickGuiModule gui = ClickGuiModule.getInstance();
String name = gui == null ? "" : gui.font.getValue();
int size = gui == null ? 10 : gui.fontSize.getValue();
if (name == null) name = "";
float scale = currentScale();
boolean changed = !Objects.equals(name, loadedName) || size != loadedSize || scale != loadedScale;
if (!changed) {
boolean missing = awtFont == null && !name.isBlank();
// retry a missing file every couple of seconds so dropping one
// into the folder loads without touching the setting again
if (!missing || System.currentTimeMillis() - lastAttempt < 2000) return;
}
reset(name, size, scale, changed);
}
private static void reset(String name, float size, float scale, boolean verbose) {
loadedName = name;
loadedSize = size;
loadedScale = scale;
lastAttempt = System.currentTimeMillis();
awtFont = null;
metrics = null;
clearGlyphs();
try {
Files.createDirectories(FONT_DIR);
} catch (IOException e) {
LOGGER.error("Could not create font directory {}", FONT_DIR, e);
return;
}
if (name.isBlank()) return;
try {
Path file = resolveFontFile(name);
if (file != null) {
applyFont(java.awt.Font.createFont(java.awt.Font.TRUETYPE_FONT, file.toFile()),
size * scale, "Loaded font " + file.getFileName(), verbose);
return;
}
java.awt.Font installed = installedFont(name);
if (installed != null) {
applyFont(installed, size * scale, "Using system font '" + name + "'", verbose);
return;
}
if (verbose) LOGGER.warn("Font '{}' not found in {}", name, FONT_DIR);
} catch (Exception e) {
LOGGER.error("Failed to load font '{}'", name, e);
awtFont = null;
metrics = null;
}
}
private static void applyFont(java.awt.Font font, float size, String source, boolean verbose) {
awtFont = font.deriveFont(java.awt.Font.PLAIN, size);
BufferedImage probe = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = probe.createGraphics();
graphics.setFont(awtFont);
metrics = graphics.getFontMetrics();
// distance from the top of the line box to the middle of the cap band:
// drawString puts the baseline at metrics.getAscent(), and "A" spans
// from the baseline up to the cap line
java.awt.geom.Rectangle2D capBounds = awtFont
.createGlyphVector(graphics.getFontRenderContext(), "A")
.getVisualBounds();
float capHeight = (float) capBounds.getHeight();
if (capHeight <= 0f || capHeight > metrics.getAscent()) {
capHeight = metrics.getAscent();
}
visualCenterRaster = metrics.getAscent() - capHeight / 2f;
graphics.dispose();
if (verbose) LOGGER.info("{} ({}pt)", source, size);
}
/**
* Distance from the top of a line box down to the visual center of its
* text (middle of the cap band), in gui units. lets callers center labels
* by what the eye sees instead of the line box, and behaves the same for
* the custom font and the vanilla fallback.
*/
public static float visualCenter() {
sync();
if (awtFont == null) return mc.font.lineHeight / 2f - 1f;
return visualCenterRaster / loadedScale;
}
/**
* Resolves java's logical fonts (guaranteed on every os) and any font
* family already installed on the system, without needing a file.
*/
private static java.awt.Font installedFont(String name) {
String logical = LOGICAL_FONTS.get(name.toLowerCase(Locale.ROOT));
if (logical != null) return new java.awt.Font(logical, java.awt.Font.PLAIN, 1);
if (installedFamilies == null) {
installedFamilies = java.awt.GraphicsEnvironment
.getLocalGraphicsEnvironment()
.getAvailableFontFamilyNames();
}
for (String family : installedFamilies) {
if (family.equalsIgnoreCase(name)) return new java.awt.Font(family, java.awt.Font.PLAIN, 1);
}
return null;
}
private static Path resolveFontFile(String name) {
try {
Files.createDirectories(FONT_DIR);
} catch (IOException e) {
LOGGER.error("Could not create font directory {}", FONT_DIR, e);
return null;
}
String lower = name.toLowerCase(Locale.ROOT);
if (lower.endsWith(".ttf") || lower.endsWith(".otf")) {
Path direct = FONT_DIR.resolve(name);
return Files.isRegularFile(direct) ? direct : null;
}
try (Stream<Path> files = Files.list(FONT_DIR)) {
return files.filter(Files::isRegularFile)
.filter(path -> {
String fileName = path.getFileName().toString().toLowerCase(Locale.ROOT);
return fileName.equals(lower)
|| fileName.equals(lower + ".ttf")
|| fileName.equals(lower + ".otf");
})
.findFirst()
.orElse(null);
} catch (IOException e) {
LOGGER.error("Could not list font directory {}", FONT_DIR, e);
return null;
}
}
private static void clearGlyphs() {
for (Glyph glyph : new ArrayList<>(GLYPHS.values())) {
queueRelease(glyph.id());
}
GLYPHS.clear();
}
private static void queueRelease(Identifier id) {
lastQueuedAt = System.currentTimeMillis();
PENDING_RELEASE.add(id);
}
private static void flushReleases() {
if (PENDING_RELEASE.isEmpty()) return;
if (System.currentTimeMillis() - lastQueuedAt < 250) return;
for (Identifier id : PENDING_RELEASE) {
mc.getTextureManager().release(id);
}
PENDING_RELEASE.clear();
}
// --- rasterizing ---
private static void drawGlyphs(GuiGraphics context, String text, int x, int y, int color, boolean shadow) {
flushReleases();
Key key = new Key(color, text, shadow);
Glyph glyph = GLYPHS.get(key);
if (glyph == null) {
glyph = createGlyph(text, color, shadow);
if (glyph == null) return;
GLYPHS.put(key, glyph);
}
// the bitmap is rasterized at device resolution (one texel per screen
// pixel); scale the pose down into gui space so the gpu never has to
// magnify it with nearest-neighbour sampling
float scale = loadedScale;
context.pose().pushMatrix();
context.pose().scale(1f / scale, 1f / scale);
context.pose().translate((x - 1) * scale, (y - 1) * scale);
context.blit(RenderPipelines.GUI_TEXTURED, glyph.id(),
0, 0, 0.0f, 0.0f, glyph.width(), glyph.height(), glyph.width(), glyph.height());
context.pose().popMatrix();
}
private static Glyph createGlyph(String text, int baseColor, boolean shadow) {
String plain = stripCodes(text);
if (plain.isEmpty()) return null;
int pad = Math.max(1, Math.round(loadedScale));
int width = Math.max(1, metrics.stringWidth(plain));
int height = metrics.getHeight();
int imageWidth = width + 2 * pad;
int imageHeight = height + 2 * pad;
BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = image.createGraphics();
graphics.setFont(awtFont);
graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
drawSegments(graphics, text, baseColor, shadow);
graphics.dispose();
NativeImage nativeImage = new NativeImage(imageWidth, imageHeight, false);
for (int py = 0; py < imageHeight; py++) {
for (int px = 0; px < imageWidth; px++) {
int argb = image.getRGB(px, py);
int abgr = (argb & 0xFF000000)
| ((argb & 0x000000FF) << 16)
| (argb & 0x0000FF00)
| ((argb & 0x00FF0000) >> 16);
nativeImage.setPixelABGR(px, py, abgr);
}
}
Identifier id = Identifier.fromNamespaceAndPath("fros", "font/glyph_" + glyphCounter++);
DynamicTexture texture = new DynamicTexture(() -> id.toString(), nativeImage);
texture.upload();
mc.getTextureManager().register(id, texture);
return new Glyph(id, imageWidth, imageHeight);
}
private static void drawSegments(Graphics2D graphics, String text, int baseColor, boolean shadow) {
int baseline = metrics.getAscent() + Math.round(loadedScale);
int cursor = 1;
int current = baseColor;
int segmentStart = 0;
int i = 0;
while (i < text.length()) {
if (text.charAt(i) == '\u00A7' && i + 1 < text.length()) {
// colored segments darken like vanilla's shadows do, instead
// of punching through at full brightness
cursor = flush(graphics, text, segmentStart, i, cursor,
shadow ? shadowColor(current) : current, baseline);
char code = Character.toLowerCase(text.charAt(i + 1));
i += 2;
segmentStart = i;
if (code == 'x') {
int rgb = 0;
int nibbles = 0;
while (nibbles < 6 && i + 1 < text.length() && text.charAt(i) == '\u00A7') {
int digit = Character.digit(Character.toLowerCase(text.charAt(i + 1)), 16);
if (digit < 0) break;
rgb = (rgb << 4) | digit;
i += 2;
nibbles++;
}
current = 0xFF000000 | rgb;
segmentStart = i;
} else if (code == 'r') {
current = baseColor;
} else {
int index = Character.digit(code, 16);
if (index >= 0) current = 0xFF000000 | PALETTE[index];
}
continue;
}
i++;
}
flush(graphics, text, segmentStart, text.length(), cursor,
shadow ? shadowColor(current) : current, baseline);
}
private static int flush(Graphics2D graphics, String text, int start, int end, int cursor, int color, int baseline) {
if (end <= start) return cursor;
String segment = text.substring(start, end);
graphics.setColor(new Color(color, true));
graphics.drawString(segment, cursor, baseline);
return cursor + metrics.stringWidth(segment);
}
// --- helpers ---
private static String stripCodes(String text) {
if (text.indexOf('\u00A7') < 0) return text;
StringBuilder builder = new StringBuilder(text.length());
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (c == '\u00A7' && i + 1 < text.length()) {
i++;
continue;
}
builder.append(c);
}
return builder.toString();
}
private static String toCodeString(Component component) {
StringBuilder builder = new StringBuilder();
TextColor[] previous = {null};
component.visit((style, text) -> {
TextColor color = style.getColor();
if (Objects.equals(color, previous[0])) {
builder.append(text);
return Optional.empty();
}
if (color == null) {
builder.append('\u00A7').append('r');
} else {
appendColorCode(builder, color.getValue());
}
previous[0] = color;
builder.append(text);
return Optional.empty();
}, Style.EMPTY);
return builder.toString();
}
private static void appendColorCode(StringBuilder builder, int rgb) {
for (int i = 0; i < PALETTE.length; i++) {
if (PALETTE[i] == (rgb & 0xFFFFFF)) {
builder.append('\u00A7').append(HEX[i]);
return;
}
}
builder.append('\u00A7').append('x');
for (int i = 5; i >= 0; i--) {
builder.append('\u00A7').append(HEX[(rgb >> (i * 4)) & 0xF]);
}
}
private static int shadowColor(int color) {
return ((color & 0xFCFCFC) >> 2) | (color & 0xFF000000);
}
}