chore(history): squash 13 commit(s) from 2026-08-01

- refactor: Фаза 3 — разбить CustomBar (1495→426 фасад + 11 хелперов ≤200)
- docs: Фаза 3 завершена — обновить TODO (8 гигантов разбито, долг по >200)
- refactor: Фаза 4 — Security: BackdoorProtection инстанс-сервис, удалить StaffHeuristics
- refactor: Фаза 5 — нормализация модулей: ленивый Minecraft, GameClock
- feat: ShaderSky, Chams hand shaders + dual, BackSword, TargetESP blackhole, TargetManager 10s target
- fix: GUI — убрать пустую категорию Movement, починить HUD в настройках
- feat: кнопка-лупа поиска в меню модулей
- fix: крэш без Sodium (BedwarsESP/BlockObservationHub) + тесты на настройки HUD
- fix: диагностика HUD-виджетов в консоль (почему вкладка HUD пустая)
- fix: подробная диагностика скана HUD-виджетов (found/registered/errors)
- fix: HUD-виджеты загружаются лениво (вкладки HUD/UI пустые in-game)
- fix: HUD-виджеты не регистрировались in-game — classloader ClassGraph vs Knot
- fix: убрать дебаг-логи HUD (System.out)
This commit is contained in:
loki5512344 2026-08-01 17:06:34 +02:00
parent 52d87368c9
commit ff42902c43
90 changed files with 4290 additions and 1949 deletions

View file

@ -3,6 +3,7 @@ package dev.loki.lovisual.features.gui.clickgui;
import dev.loki.lovisual.features.gui.clickgui.sections.settings.SettingsTabRuntime;
import dev.loki.lovisual.render.engine.math.HudScale;
import dev.loki.lovisual.util.wav.ClickGuiSounds;
import net.minecraft.client.Minecraft;
final class ClickGuiInputHandler {
@ -163,18 +164,18 @@ final class ClickGuiInputHandler {
}
private static void updateMouse(double x, double y, boolean fromScaledCoords) {
if (ClickGuiRenderer.MC == null || ClickGuiRenderer.MC.getWindow() == null) {
if (Minecraft.getInstance() == null || Minecraft.getInstance().getWindow() == null) {
ClickGuiRenderer.mouseX = (float) x;
ClickGuiRenderer.mouseY = (float) y;
return;
}
if (fromScaledCoords) {
double scaledW = ClickGuiRenderer.MC.getWindow().getGuiScaledWidth();
double scaledH = ClickGuiRenderer.MC.getWindow().getGuiScaledHeight();
double fbW = ClickGuiRenderer.MC.getWindow().getWidth();
double fbH = ClickGuiRenderer.MC.getWindow().getHeight();
double sx = scaledW > 0 ? fbW / scaledW : ClickGuiRenderer.MC.getWindow().getGuiScale();
double sy = scaledH > 0 ? fbH / scaledH : ClickGuiRenderer.MC.getWindow().getGuiScale();
double scaledW = Minecraft.getInstance().getWindow().getGuiScaledWidth();
double scaledH = Minecraft.getInstance().getWindow().getGuiScaledHeight();
double fbW = Minecraft.getInstance().getWindow().getWidth();
double fbH = Minecraft.getInstance().getWindow().getHeight();
double sx = scaledW > 0 ? fbW / scaledW : Minecraft.getInstance().getWindow().getGuiScale();
double sy = scaledH > 0 ? fbH / scaledH : Minecraft.getInstance().getWindow().getGuiScale();
double fbX = x * sx;
double fbY = y * sy;
float uiScale = HudScale.scale((int) fbW, (int) fbH);
@ -182,9 +183,9 @@ final class ClickGuiInputHandler {
ClickGuiRenderer.mouseY = (float) (fbY / uiScale);
return;
}
double scale = ClickGuiRenderer.MC.getWindow().getGuiScale();
double fbW = ClickGuiRenderer.MC.getWindow().getWidth();
double fbH = ClickGuiRenderer.MC.getWindow().getHeight();
double scale = Minecraft.getInstance().getWindow().getGuiScale();
double fbW = Minecraft.getInstance().getWindow().getWidth();
double fbH = Minecraft.getInstance().getWindow().getHeight();
double fbX = x * scale;
double fbY = y * scale;
float uiScale = HudScale.scale((int) fbW, (int) fbH);

View file

@ -5,6 +5,7 @@ import dev.loki.lovisual.features.module.ModuleManager;
import dev.loki.lovisual.render.engine.animation.AnimationUtility;
import dev.loki.lovisual.render.helpers.SystemCursor;
import dev.loki.lovisual.util.screen.ClientScreen;
import net.minecraft.client.Minecraft;
import org.lwjgl.glfw.GLFW;
final class ClickGuiLifecycle {
@ -50,14 +51,14 @@ final class ClickGuiLifecycle {
}
private static void detachScreenForExitOverlay() {
if (ClickGuiRenderer.MC == null) return;
if (Minecraft.getInstance() == null) return;
if (ClientScreen.current() instanceof ClickGuiScreen cs) {
ClickGuiRenderer.mainScreen = cs;
}
if (ClientScreen.current() instanceof ClickGuiScreen
|| ClientScreen.current() instanceof ClickGuiPickerScreen
|| ClientScreen.current() instanceof ClickGuiEditorScreen) {
ClientScreen.show(ClickGuiRenderer.MC, null);
ClientScreen.show(Minecraft.getInstance(), null);
}
}
@ -82,7 +83,7 @@ final class ClickGuiLifecycle {
static void onClose() {
if (!ClickGuiRenderer.cursorShown) return;
long handle = ClickGuiRenderer.MC.getWindow().handle();
long handle = Minecraft.getInstance().getWindow().handle();
GLFW.glfwSetInputMode(handle, GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_DISABLED);
SystemCursor.invalidate();
@ -97,17 +98,17 @@ final class ClickGuiLifecycle {
}
static void releaseCursorForGameplay() {
if (ClickGuiRenderer.MC == null || ClickGuiRenderer.MC.getWindow() == null) return;
if (Minecraft.getInstance() == null || Minecraft.getInstance().getWindow() == null) return;
if (ClientScreen.current() == null) {
try {
ClickGuiRenderer.MC.mouseHandler.grabMouse();
Minecraft.getInstance().mouseHandler.grabMouse();
SystemCursor.invalidate();
} catch (Throwable ignored) {
GLFW.glfwSetInputMode(ClickGuiRenderer.MC.getWindow().handle(), GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_DISABLED);
GLFW.glfwSetInputMode(Minecraft.getInstance().getWindow().handle(), GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_DISABLED);
SystemCursor.invalidate();
}
} else {
GLFW.glfwSetInputMode(ClickGuiRenderer.MC.getWindow().handle(), GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_DISABLED);
GLFW.glfwSetInputMode(Minecraft.getInstance().getWindow().handle(), GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_DISABLED);
SystemCursor.invalidate();
}
ClickGuiRenderer.cursorShown = false;
@ -116,9 +117,9 @@ final class ClickGuiLifecycle {
static void ensureCursorShown() {
if (ClickGuiRenderer.cursorShown) return;
if (ClickGuiRenderer.MC == null || ClickGuiRenderer.MC.getWindow() == null) return;
if (Minecraft.getInstance() == null || Minecraft.getInstance().getWindow() == null) return;
GLFW.glfwSetInputMode(
ClickGuiRenderer.MC.getWindow().handle(),
Minecraft.getInstance().getWindow().handle(),
GLFW.GLFW_CURSOR,
GLFW.GLFW_CURSOR_NORMAL
);
@ -145,10 +146,10 @@ final class ClickGuiLifecycle {
ClickGuiRenderer.finishingClose = true;
ClickGuiRenderer.closingForExit = false;
onClose();
if (ClickGuiRenderer.MC != null && (ClientScreen.current() instanceof ClickGuiScreen
if (Minecraft.getInstance() != null && (ClientScreen.current() instanceof ClickGuiScreen
|| ClientScreen.current() instanceof ClickGuiPickerScreen
|| ClientScreen.current() instanceof ClickGuiEditorScreen)) {
ClientScreen.show(ClickGuiRenderer.MC, null);
ClientScreen.show(Minecraft.getInstance(), null);
}
ClickGuiScreenManager.clearScreens();
ClickGuiRenderer.finishingClose = false;

View file

@ -3,6 +3,7 @@ package dev.loki.lovisual.features.gui.clickgui;
import dev.loki.lovisual.mixininterface.IGuiGraphics;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.helpers.ScissorFunction;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.world.item.ItemStack;
@ -26,7 +27,7 @@ final class ClickGuiPickerIconManager {
ClickGuiRenderer.pickerIconScissorActive = false;
return;
}
if (ClickGuiRenderer.MC == null) {
if (Minecraft.getInstance() == null) {
ClickGuiRenderer.pickerIcons.clear();
ClickGuiRenderer.pickerIconScissorActive = false;
return;

View file

@ -9,6 +9,7 @@ import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.text.TextRenderer;
import dev.loki.lovisual.render.helpers.SystemCursor;
import dev.loki.lovisual.util.screen.ClientScreen;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import org.lwjgl.glfw.GLFW;
@ -49,15 +50,15 @@ final class ClickGuiRenderEngine {
try {
if (enabled && !ClickGuiRenderer.closingForExit && !ClickGuiRenderer.cursorShown) {
GLFW.glfwSetInputMode(
ClickGuiRenderer.MC.getWindow().handle(),
Minecraft.getInstance().getWindow().handle(),
GLFW.GLFW_CURSOR,
GLFW.GLFW_CURSOR_NORMAL
);
SystemCursor.invalidate();
ClickGuiRenderer.cursorShown = true;
}
int fbw = ClickGuiRenderer.MC.getWindow().getWidth();
int fbh = ClickGuiRenderer.MC.getWindow().getHeight();
int fbw = Minecraft.getInstance().getWindow().getWidth();
int fbh = Minecraft.getInstance().getWindow().getHeight();
int vw = Math.max(1, Math.round(HudScale.virtualWidth(fbw, fbh)));
int vh = Math.max(1, Math.round(HudScale.virtualHeight(fbw, fbh)));
ClickGuiRenderer.fbWidth = vw;

View file

@ -15,7 +15,6 @@ import dev.loki.lovisual.features.gui.clickgui.sections.SettingsSection;
import dev.loki.lovisual.features.gui.clickgui.settings.*;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.text.TextRenderer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.world.item.ItemStack;
@ -26,7 +25,6 @@ import java.util.concurrent.CompletableFuture;
public enum ClickGuiRenderer {
;
static final Minecraft MC = Minecraft.getInstance();
static final ModulesSection MODULES_SECTION = new ModulesSection();
static final SettingsSection SETTINGS_SECTION = new SettingsSection();
static final String MODULES_TAB_ID = "lovisual:modules";

View file

@ -8,6 +8,7 @@ import dev.loki.lovisual.features.gui.clickgui.settings.ProtocolHeuristicsSettin
import dev.loki.lovisual.features.gui.clickgui.settings.TextListSetting;
import dev.loki.lovisual.util.screen.ClientScreen;
import dev.loki.lovisual.util.wav.ClickGuiSounds;
import net.minecraft.client.Minecraft;
import java.util.concurrent.CompletableFuture;
@ -17,17 +18,17 @@ final class ClickGuiScreenManager {
}
static void openClickGuiScreen() {
if (ClickGuiRenderer.MC == null) return;
if (Minecraft.getInstance() == null) return;
if (ClientScreen.current() != null) return;
if (ClickGuiRenderer.mainScreen == null) {
ClickGuiRenderer.mainScreen = new ClickGuiScreen();
}
ClickGuiSounds.guiOpen();
ClientScreen.show(ClickGuiRenderer.MC, ClickGuiRenderer.mainScreen);
ClientScreen.show(Minecraft.getInstance(), ClickGuiRenderer.mainScreen);
}
static void captureMainScreen() {
if (ClickGuiRenderer.MC != null && ClientScreen.current() instanceof ClickGuiScreen cs) {
if (Minecraft.getInstance() != null && ClientScreen.current() instanceof ClickGuiScreen cs) {
ClickGuiRenderer.mainScreen = cs;
}
}
@ -40,14 +41,14 @@ final class ClickGuiScreenManager {
}
private static void openPickerScreen() {
if (ClickGuiRenderer.MC == null) return;
if (Minecraft.getInstance() == null) return;
if (ClientScreen.current() instanceof ClickGuiScreen cs) {
ClickGuiRenderer.mainScreen = cs;
}
if (ClickGuiRenderer.pickerScreen == null) {
ClickGuiRenderer.pickerScreen = new ClickGuiPickerScreen();
}
ClientScreen.show(ClickGuiRenderer.MC, ClickGuiRenderer.pickerScreen);
ClientScreen.show(Minecraft.getInstance(), ClickGuiRenderer.pickerScreen);
}
static void clearScreens() {
@ -86,11 +87,11 @@ final class ClickGuiScreenManager {
ClickGuiRenderer.picker = null;
ClickGuiRenderer.protocolHeuristicsEditor = null;
ClickGuiRenderer.textEditor = null;
if (ClickGuiRenderer.MC != null && ClientScreen.current() instanceof ClickGuiPickerScreen) {
if (Minecraft.getInstance() != null && ClientScreen.current() instanceof ClickGuiPickerScreen) {
if (ClickGuiRenderer.mainScreen == null) {
ClickGuiRenderer.mainScreen = new ClickGuiScreen();
}
ClientScreen.show(ClickGuiRenderer.MC, ClickGuiRenderer.mainScreen);
ClientScreen.show(Minecraft.getInstance(), ClickGuiRenderer.mainScreen);
}
}

View file

@ -10,6 +10,7 @@ import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.text.TextRenderer;
import dev.loki.lovisual.render.helpers.SystemCursor;
import dev.loki.lovisual.util.screen.ClientScreen;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
final class ClickGuiTopLayer {
@ -18,7 +19,7 @@ final class ClickGuiTopLayer {
}
static boolean shouldRenderAsTopLayer() {
return ClickGuiRenderer.MC != null && (ClickGuiLifecycle.isLifecycleActive()
return Minecraft.getInstance() != null && (ClickGuiLifecycle.isLifecycleActive()
|| ClientScreen.current() instanceof ClickGuiScreen
|| ClientScreen.current() instanceof ClickGuiPickerScreen
|| ClientScreen.current() instanceof ClickGuiEditorScreen);
@ -50,11 +51,11 @@ final class ClickGuiTopLayer {
}
static void renderEditor(Renderer2D rendererIn, TextRenderer fallback, GuiGraphicsExtractor ctx, float tickDelta) {
if (ClickGuiRenderer.MC == null || ClickGuiRenderer.MC.getWindow() == null) return;
if (Minecraft.getInstance() == null || Minecraft.getInstance().getWindow() == null) return;
ClickGuiLifecycle.init();
ClickGuiRenderer.renderer = rendererIn;
int fbw = ClickGuiRenderer.MC.getWindow().getWidth();
int fbh = ClickGuiRenderer.MC.getWindow().getHeight();
int fbw = Minecraft.getInstance().getWindow().getWidth();
int fbh = Minecraft.getInstance().getWindow().getHeight();
int vw = Math.max(1, Math.round(HudScale.virtualWidth(fbw, fbh)));
int vh = Math.max(1, Math.round(HudScale.virtualHeight(fbw, fbh)));
ClickGuiRenderer.fbWidth = vw;

View file

@ -15,7 +15,6 @@ import java.util.Locale;
public enum ModulesMenuCategory {
COMBAT("Combat", "swords", "combat"),
MOVEMENT("Movement", "accessibility", "movement"),
PLAYER("Player", "user", "player"),
VISUALS("Visuals", "tree-pine", "visual", "render", "visuals"),
OTHER("Other", "ellipsis", "other", "misc", "miscellaneous", "exploit", "world");

View file

@ -63,6 +63,11 @@ public final class ModulesMenuScreen {
private float searchY;
private float searchW;
private float searchH;
private float searchBtnX;
private float searchBtnY;
private float searchBtnW;
private float searchBtnH;
private float searchBtnHoverAnim;
private TextRenderer regular;
private TextRenderer medium;
@ -207,6 +212,11 @@ public final class ModulesMenuScreen {
float hiddenSearchY = visibleSearchY + 12.0f * scale;
searchY = AnimationUtility.lerp(hiddenSearchY, visibleSearchY, searchAnim);
searchBtnW = 22.0f * scale;
searchBtnH = 22.0f * scale;
searchBtnX = areaX + areaW - searchBtnW - 14.0f * scale;
searchBtnY = areaY + 12.0f * scale;
}
public void render(float mouseX, float mouseY) {
@ -246,6 +256,7 @@ public final class ModulesMenuScreen {
}
}
renderSearchButton(mouseX, mouseY);
renderSearch(mouseX, mouseY);
renderHints();
@ -268,6 +279,11 @@ public final class ModulesMenuScreen {
public boolean mousePressed(float mouseX, float mouseY, int button) {
if (!isInteractive()) return false;
if (button == GLFW.GLFW_MOUSE_BUTTON_LEFT && inside(mouseX, mouseY, searchBtnX, searchBtnY, searchBtnW, searchBtnH)) {
ClickGuiSearch.setActive(!ClickGuiSearch.isActive());
return true;
}
if (button == GLFW.GLFW_MOUSE_BUTTON_LEFT && inside(mouseX, mouseY, searchX, searchY, searchW, searchH)) {
ClickGuiSearch.setActive(true);
return true;
@ -726,6 +742,36 @@ public final class ModulesMenuScreen {
if (pageClip) ScissorFunction.pop();
}
private void renderSearchButton(float mouseX, float mouseY) {
float alpha = easeOutCubic(screenAnim);
if (alpha <= 0.001f) return;
boolean hovered = inside(mouseX, mouseY, searchBtnX, searchBtnY, searchBtnW, searchBtnH);
float dt = AnimationUtility.deltaTime();
searchBtnHoverAnim = AnimationUtility.approach(searchBtnHoverAnim, hovered ? 1.0f : 0.0f, dt, 12.0f);
searchBtnHoverAnim = AnimationUtility.snap(searchBtnHoverAnim, hovered ? 1.0f : 0.0f, 0.01f);
int bg = withAlpha(ModulesMenuStyle.panelBgGlassDark(), alpha * (0.45f + 0.35f * searchBtnHoverAnim));
LayoutRender2D.roundedQuad(searchBtnX, searchBtnY, searchBtnW, searchBtnH, 6.0f * scale, bg, bg, bg, bg);
LayoutRender2D.roundedStrokeQuad(
searchBtnX, searchBtnY, searchBtnW, searchBtnH, 6.0f * scale, 0.5f * scale,
withAlpha(ModulesMenuStyle.textMuted(), alpha * 0.35f),
withAlpha(ModulesMenuStyle.textMuted(), alpha * 0.35f),
withAlpha(ModulesMenuStyle.textMuted(), alpha * 0.35f),
withAlpha(ModulesMenuStyle.textMuted(), alpha * 0.35f)
);
TextRenderer icons = Fonts.renderer("Icons", FontInfo.Type.Regular, ClickGuiRenderer.getInterRegular());
float iconSize = 9.0f * scale;
float iconX = searchBtnX + (searchBtnW - ClickGuiRenderer.textWidth(icons, "s", iconSize)) * 0.5f;
float iconY = searchBtnY + middle(textHeight(icons, iconSize), searchBtnH);
int iconColor = withAlpha(
ClickGuiSearch.isActive() ? 0xFFF5F5FF : hovered ? 0xFFD0D0DC : 0xFF878894,
alpha
);
ClickGuiRenderer.drawText(icons, "s", iconX, iconY, iconSize, iconColor, false);
}
private void renderSearch(float mouseX, float mouseY) {
if (searchAnim <= 0.001f && !ClickGuiSearch.isActive()) return;

View file

@ -63,15 +63,19 @@ public enum MenuSettingsResolver {
case HUD -> {
for (DraggableHudElement widget : DraggableHudElementRegistry.getWidgets()) {
if (widget == null) continue;
out.add(new ModuleComponent.CardEntry(
widget.getId(),
widget.getTitle(),
"",
"",
!widget.getSettingDefs().isEmpty(),
widget.isEnabled(),
true
));
try {
out.add(new ModuleComponent.CardEntry(
widget.getId(),
widget.getTitle(),
"",
"",
!widget.getSettingDefs().isEmpty(),
widget.isEnabled(),
true
));
} catch (Throwable ignored) {
// один сломанный виджет не должен опустошать всю категорию HUD
}
}
}
case UI -> {

View file

@ -15,7 +15,6 @@ import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.Lay
import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiMath;
import dev.loki.lovisual.features.module.modules.misc.DefineTarget;
import dev.loki.lovisual.features.relations.PlayerRelations;
import dev.loki.lovisual.features.relations.StaffHeuristicsConfig;
import dev.loki.lovisual.render.engine.animation.AnimationUtility;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.svg.SvgRenderOptions;
@ -38,14 +37,10 @@ public final class RelationsComponent {
private final RelationPlayerCardComponent cardComponent = new RelationPlayerCardComponent();
private final OnlineRelationPlayerPickerComponent onlinePicker;
private final List<CardEntryHit> cardHits = new ArrayList<>();
private final List<ChipHit> chipHits = new ArrayList<>();
private RelationTab tab = RelationTab.FRIENDS;
private ActiveField activeField = ActiveField.NONE;
private String playerInput = "";
private String prefixInput = "";
private String suffixInput = "";
private String containsInput = "";
private String statusMessage;
private long statusUntilMs;
@ -55,13 +50,6 @@ public final class RelationsComponent {
private Rect playerInputRect = Rect.ZERO;
private Rect addTypedButton = Rect.ZERO;
private Rect reservedPickerButton = Rect.ZERO;
private Rect enabledToggle = Rect.ZERO;
private Rect prefixInputRect = Rect.ZERO;
private Rect suffixInputRect = Rect.ZERO;
private Rect containsInputRect = Rect.ZERO;
private Rect prefixAddButton = Rect.ZERO;
private Rect suffixAddButton = Rect.ZERO;
private Rect containsAddButton = Rect.ZERO;
private float scroll;
private float smoothedScroll;
@ -107,7 +95,6 @@ public final class RelationsComponent {
public void render(float menuX, float menuY, float menuW, float menuH, float mx, float my, float scale) {
cardHits.clear();
chipHits.clear();
SettingsGuiPalette palette = SettingsGuiPalette.current();
movementInputBlocked = activeField != ActiveField.NONE || onlinePicker.blocksMovementInput();
@ -131,12 +118,6 @@ public final class RelationsComponent {
return;
}
if (tab == RelationTab.STAFF) {
float heurH = 78f * scale;
renderHeuristicsPanel(areaX, y, areaW, heurH, mx, my, scale, palette);
y += heurH + 8f * scale;
}
listX = areaX;
listY = y;
listW = areaW;
@ -211,39 +192,6 @@ public final class RelationsComponent {
if (onlinePicker.isVisible()) {
return false;
}
if (tab == RelationTab.STAFF) {
if (enabledToggle.contains(mx, my)) {
StaffHeuristicsConfig cfg = StaffHeuristicsConfig.get();
cfg.setEnabled(!cfg.enabled());
return true;
}
if (prefixInputRect.contains(mx, my)) {
activeField = ActiveField.PREFIX;
movementInputBlocked = true;
clearStatus();
return true;
}
if (suffixInputRect.contains(mx, my)) {
activeField = ActiveField.SUFFIX;
movementInputBlocked = true;
clearStatus();
return true;
}
if (containsInputRect.contains(mx, my)) {
activeField = ActiveField.CONTAINS;
movementInputBlocked = true;
clearStatus();
return true;
}
if (prefixAddButton.contains(mx, my)) return commitHeuristic(ActiveField.PREFIX);
if (suffixAddButton.contains(mx, my)) return commitHeuristic(ActiveField.SUFFIX);
if (containsAddButton.contains(mx, my)) return commitHeuristic(ActiveField.CONTAINS);
for (ChipHit hit : chipHits) {
if (!hit.delete().contains(mx, my)) continue;
removeHeuristic(hit.kind(), hit.value());
return true;
}
}
for (CardEntryHit entryHit : cardHits) {
CardHit hit = entryHit.hit();
@ -274,8 +222,7 @@ public final class RelationsComponent {
return true;
}
if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) {
if (activeField == ActiveField.PLAYER) commitPlayerInput();
else commitHeuristic(activeField);
commitPlayerInput();
return true;
}
if (keyCode == GLFW.GLFW_KEY_BACKSPACE) {
@ -365,80 +312,6 @@ public final class RelationsComponent {
drawIconButton(reservedPickerButton, onlineMode ? "x" : "user-plus", pickerHoverAnim, false, scale, palette);
}
private void renderHeuristicsPanel(float x, float y, float w, float h, float mx, float my, float scale, SettingsGuiPalette palette) {
StaffHeuristicsConfig cfg = StaffHeuristicsConfig.get();
ClickGuiRenderer.drawBlur(x, y, w, h, 5f * scale, 0xFF000000, 190f / 255f);
LayoutRender2D.roundedQuad(x, y, w, h, 5f * scale,
palette.moduleCardTop(), palette.moduleCardTopStrong(), palette.moduleCardBottom(), palette.moduleCardBottomStrong());
LayoutRender2D.roundedStroke(x, y, w, h, 5f * scale, 0.5f * scale, palette.menuWindowStroke());
ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), tr("heuristics.title", "Staff heuristics"), x + 8f * scale, y + 6f * scale, 8f * scale, palette.moduleTitleText(), false);
enabledToggle = new Rect(x + w - 56f * scale, y + 5f * scale, 48f * scale, 14f * scale);
drawToggle(enabledToggle, cfg.enabled(), mx, my, scale, palette);
float top = y + 25f * scale;
float gap = 7f * scale;
float colW = (w - 16f * scale - gap * 2f) / 3f;
renderHeuristicColumn(ActiveField.PREFIX, tr("heuristics.prefixes", "Prefixes"), cfg.prefixes(), x + 8f * scale, top, colW, h - 31f * scale, mx, my, scale, palette);
renderHeuristicColumn(ActiveField.SUFFIX, tr("heuristics.suffixes", "Suffixes"), cfg.suffixes(), x + 8f * scale + colW + gap, top, colW, h - 31f * scale, mx, my, scale, palette);
renderHeuristicColumn(ActiveField.CONTAINS, tr("heuristics.contains", "Contains"), cfg.contains(), x + 8f * scale + (colW + gap) * 2f, top, colW, h - 31f * scale, mx, my, scale, palette);
}
private void renderHeuristicColumn(ActiveField kind,
String title,
Set<String> entries,
float x,
float y,
float w,
float h,
float mx,
float my,
float scale,
SettingsGuiPalette palette) {
ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), title, x, y, 6.7f * scale, palette.panelMuted(), false);
Rect field = new Rect(x, y + 10f * scale, w - 16f * scale, 14f * scale);
Rect add = new Rect(x + w - 14f * scale, y + 10f * scale, 14f * scale, 14f * scale);
if (kind == ActiveField.PREFIX) {
prefixInputRect = field;
prefixAddButton = add;
} else if (kind == ActiveField.SUFFIX) {
suffixInputRect = field;
suffixAddButton = add;
} else {
containsInputRect = field;
containsAddButton = add;
}
drawInput(field, fieldText(kind), tr("placeholder.rule", "Rule"), activeField == kind, mx, my, scale, palette);
drawIconButton(add, "check", add.contains(mx, my) ? 1f : 0f, false, scale, palette);
float chipX = x;
float chipY = y + 29f * scale;
float chipH = 11f * scale;
List<String> sorted = sorted(entries);
for (String entry : sorted) {
float textSize = 5.9f * scale;
String fitted = ClickGuiRenderer.fitText(ClickGuiRenderer.getInterRegular(), entry, textSize, w - 14f * scale);
float chipW = Math.min(w, ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterRegular(), fitted, textSize) + 16f * scale);
if (chipX + chipW > x + w + 0.1f) {
chipX = x;
chipY += chipH + 3f * scale;
}
if (chipY + chipH > y + h) break;
Rect chip = new Rect(chipX, chipY, chipW, chipH);
Rect del = new Rect(chipX + chipW - 10f * scale, chipY + 1f * scale, 9f * scale, 9f * scale);
LayoutRender2D.roundedQuad(chip.x(), chip.y(), chip.w(), chip.h(), 3f * scale,
LayoutRender2D.alpha(palette.panelPillBase(), 0.95f),
LayoutRender2D.alpha(palette.panelPillBase(), 0.88f),
LayoutRender2D.alpha(palette.panelPillBase(), 0.88f),
LayoutRender2D.alpha(palette.panelPillBase(), 0.95f));
ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), fitted, chipX + 4f * scale, chipY + 3f * scale, textSize, palette.panelText(), false);
Renderer2D.COLOR.svg("x", del.x() + 1.8f * scale, del.y() + 1.8f * scale, 5.4f * scale, 5.4f * scale,
SvgRenderOptions.overrideColor(palette.panelMuted()));
chipHits.add(new ChipHit(kind, entry, del));
chipX += chipW + 3f * scale;
}
}
private void renderCards(float x, float y, float w, float h, List<String> entries, float mx, float my, float scale, SettingsGuiPalette palette) {
float gap = 6f * scale;
float reservedScrollbarW = 9f * scale;
@ -565,19 +438,6 @@ public final class RelationsComponent {
iconSize, iconSize, SvgRenderOptions.overrideColor(iconColor));
}
private void drawToggle(Rect rect, boolean enabled, float mx, float my, float scale, SettingsGuiPalette palette) {
boolean hover = rect.contains(mx, my);
int bg = enabled
? SettingsGuiPalette.mix(palette.panelPillActive(), palette.menuCategorySelectedRight(), hover ? 0.22f : 0.12f)
: SettingsGuiPalette.mix(palette.panelPillBase(), palette.panelBgRight(), hover ? 0.22f : 0.12f);
LayoutRender2D.roundedQuad(rect.x(), rect.y(), rect.w(), rect.h(), 4f * scale, bg, bg, SettingsGuiPalette.darken(bg, 0.10f), bg);
String label = enabled ? tr("heuristics.enabled", "Enabled") : tr("heuristics.disabled", "Disabled");
float size = 6.2f * scale;
float tw = ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterMedium(), label, size);
ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), label, rect.x() + (rect.w() - tw) * 0.5f, rect.y() + 4.1f * scale, size,
enabled ? palette.panelText() : palette.panelMuted(), false);
}
private boolean switchTab(RelationTab next) {
if (tab != next) {
tab = next;
@ -607,39 +467,6 @@ public final class RelationsComponent {
}
}
private boolean commitHeuristic(ActiveField kind) {
String value = StaffHeuristicsConfig.cleanRule(fieldText(kind));
if (value.isBlank()) {
setStatus(tr("status.rule_empty", "Rule is empty"));
return true;
}
boolean changed = switch (kind) {
case PREFIX -> StaffHeuristicsConfig.get().addPrefix(value);
case SUFFIX -> StaffHeuristicsConfig.get().addSuffix(value);
case CONTAINS -> StaffHeuristicsConfig.get().addContains(value);
default -> false;
};
if (changed) {
setFieldText(kind, "");
activeField = ActiveField.NONE;
movementInputBlocked = false;
setStatus(tr("status.rule_added", "Rule added"));
} else {
setStatus(tr("status.rule_exists", "Rule already exists"));
}
return true;
}
private void removeHeuristic(ActiveField kind, String value) {
boolean changed = switch (kind) {
case PREFIX -> StaffHeuristicsConfig.get().removePrefix(value);
case SUFFIX -> StaffHeuristicsConfig.get().removeSuffix(value);
case CONTAINS -> StaffHeuristicsConfig.get().removeContains(value);
default -> false;
};
if (changed) setStatus(tr("status.rule_removed", "Rule removed"));
}
private List<String> filteredEntries(List<String> entries) {
if (!ClickGuiSearch.isActive() || ClickGuiSearch.getText().isBlank()) return entries;
String q = ClickGuiSearch.getText().toLowerCase(Locale.ROOT);
@ -668,7 +495,7 @@ public final class RelationsComponent {
private void appendToField(ActiveField field, String raw) {
if (raw == null || raw.isEmpty()) return;
String current = fieldText(field);
int max = field == ActiveField.PLAYER ? 32 : 48;
int max = 32;
String next = current + raw;
if (next.length() > max) next = next.substring(0, max);
setFieldText(field, next);
@ -677,9 +504,6 @@ public final class RelationsComponent {
private String fieldText(ActiveField field) {
return switch (field) {
case PLAYER -> playerInput;
case PREFIX -> prefixInput;
case SUFFIX -> suffixInput;
case CONTAINS -> containsInput;
default -> "";
};
}
@ -688,9 +512,6 @@ public final class RelationsComponent {
String next = value == null ? "" : value;
switch (field) {
case PLAYER -> playerInput = next;
case PREFIX -> prefixInput = next;
case SUFFIX -> suffixInput = next;
case CONTAINS -> containsInput = next;
default -> {
}
}
@ -777,10 +598,7 @@ public final class RelationsComponent {
private enum ActiveField {
NONE,
PLAYER,
PREFIX,
SUFFIX,
CONTAINS
PLAYER
}
private enum RelationTab {
@ -831,9 +649,6 @@ public final class RelationsComponent {
private record CardEntryHit(String name, CardHit hit) {
}
private record ChipHit(ActiveField kind, String value, Rect delete) {
}
private record Rect(float x, float y, float w, float h) {
static final Rect ZERO = new Rect(0f, 0f, 0f, 0f);

View file

@ -13,10 +13,10 @@ import net.minecraft.network.chat.Component;
public enum CooldownRender {
;
private static final Minecraft mc = Minecraft.getInstance();
private static final float SCALE = 0.75f; // немного меньше для аккуратного влезания в слот
public static void renderTime(GuiGraphicsExtractor ctx, float progress, int x, int y, float secondsLeft) {
Minecraft mc = Minecraft.getInstance();
if (mc.font == null || ctx == null) return;
// формируем текст

View file

@ -51,6 +51,8 @@ public enum HudElementAutoLoader {
for (ClassInfo info : scan.getClassesWithAnnotation(HudElementInfo.class.getName())) {
addCandidate(elements, info);
}
} catch (Throwable t) {
DebugLog.error("HUD scan failed for %s", t, basePackage);
}
elements.sort(Comparator
@ -88,7 +90,9 @@ public enum HudElementAutoLoader {
if (info.isAbstract()) return;
try {
Class<?> type = info.loadClass();
// Class.forName с classloader'ом мода (Knot), а не ClassGraph — иначе
// getAnnotation по-рефлексии не находит аннотацию из-за другого classloader'а.
Class<?> type = Class.forName(info.getName(), false, HudElementAutoLoader.class.getClassLoader());
int order = 0;
HudElementInfo elementInfo = type.getAnnotation(HudElementInfo.class);
if (elementInfo != null) {

View file

@ -7,6 +7,8 @@
package dev.loki.lovisual.features.gui.hud;
import dev.loki.lovisual.util.logging.DebugLog;
/**
* HUD widget bootstrapper.
*/
@ -14,13 +16,31 @@ public enum HudElements {
;
private static boolean initialized = false;
private static boolean loaded = false;
public static void init() {
if (initialized) return;
initialized = true;
HudGlobalConfig.get();
try {
HudGlobalConfig.get();
} catch (Throwable t) {
DebugLog.error("Failed to init HUD global config", t);
}
HudElementAutoLoader.loadDraggable("dev.loki.lovisual.features.gui.hud.draggable.impl");
ensureLoaded();
}
/**
* Ленивая загрузка виджетов при первом обращении — не зависит от порядка init-вызовов.
*/
public static synchronized void ensureLoaded() {
if (loaded) return;
loaded = true;
try {
HudElementAutoLoader.loadDraggable("dev.loki.lovisual.features.gui.hud.draggable.impl");
} catch (Throwable t) {
DebugLog.error("Failed to load draggable HUD widgets", t);
}
}
}

View file

@ -77,10 +77,12 @@ public enum DraggableHudElementRegistry {
}
public static List<DraggableHudElement> getWidgets() {
HudElements.ensureLoaded();
return Collections.unmodifiableList(WIDGETS);
}
public static DraggableHudElement getById(String id) {
HudElements.ensureLoaded();
return findById(id);
}

View file

@ -184,7 +184,7 @@ public final class Admins extends DraggableHudElement {
}
boolean chatPreview = ClientScreen.current() instanceof ChatScreen;
List<StaffData.StaffEntry> entries = filterStaff(StaffData.getEntries());
List<StaffData.StaffEntry> entries = filterStaff(StaffData.INSTANCE.getEntries());
List<AnimatedEntry> animatedEntries = animateEntries(entries);
boolean showExampleRow = entries.isEmpty() && (forceVisible || chatPreview);
boolean showWidget = !entries.isEmpty() || showExampleRow;

View file

@ -13,11 +13,24 @@ public enum StaticHudElementBootstrap {
;
private static boolean initialized = false;
private static boolean loaded = false;
public static void init() {
if (initialized) return;
initialized = true;
HudElementAutoLoader.loadStatic("dev.loki.lovisual.features.gui.hud.nondraggable.impl");
ensureLoaded();
}
/**
* Ленивая загрузка static-элементов при первом обращении.
*/
public static synchronized void ensureLoaded() {
if (loaded) return;
loaded = true;
try {
HudElementAutoLoader.loadStatic("dev.loki.lovisual.features.gui.hud.nondraggable.impl");
} catch (Throwable ignored) {
}
}
}

View file

@ -38,10 +38,12 @@ public enum StaticHudElementRegistry {
}
public static List<AbstractHudElement> getAll() {
StaticHudElementBootstrap.ensureLoaded();
return Collections.unmodifiableList(ELEMENTS);
}
public static AbstractHudElement getById(String id) {
StaticHudElementBootstrap.ensureLoaded();
if (id == null) return null;
for (AbstractHudElement e : ELEMENTS) {
if (id.equals(e.getId())) return e;

View file

@ -0,0 +1,130 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.hud.nondraggable.impl;
import dev.loki.lovisual.config.values.EnumValue;
import dev.loki.lovisual.render.engine.color.RenderColor;
import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.resources.Identifier;
import net.minecraft.world.waypoints.TrackedWaypoint;
import java.util.Locale;
import java.util.UUID;
final class CustomBarModel {
private CustomBarModel() {
}
public enum BarMode implements EnumValue.IdProvider {
LEVEL("level"),
LOCATOR("locator"),
AUTO("auto");
private final String id;
BarMode(String id) {
this.id = id;
}
@Override
public String id() {
return id;
}
}
public enum XpBarStyle implements EnumValue.IdProvider {
GRADIENT("gradient"),
RAINBOW("rainbow"),
CUSTOM("custom");
private final String id;
XpBarStyle(String id) {
this.id = id;
}
@Override
public String id() {
return id;
}
}
public enum LocatorLabelMode implements EnumValue.IdProvider {
ALWAYS("always"),
CENTER("center"),
TARGETS("targets"),
CENTER_TARGETS("center_targets");
private final String id;
LocatorLabelMode(String id) {
this.id = id;
}
@Override
public String id() {
return id;
}
}
public static final class XpTextInfo {
public final String text;
public final int x;
public final int y;
public final int color;
public final float scale;
XpTextInfo(String text, int x, int y, int color, float scale) {
this.text = text;
this.x = x;
this.y = y;
this.color = color;
this.scale = scale;
}
}
record LocatorLabel(String name, String distance, float alpha, boolean target, boolean active, String sort) {
}
record BarColors(int track,
int fillStart,
int fillEnd,
int stroke,
int trackTexture,
int fillTexture,
int locatorTexture) {
}
static final class LocatorCandidate {
final TrackedWaypoint waypoint;
final UUID uuid;
final float yaw;
final float dist;
final String name;
final String nameSort;
final String key;
final String keySort;
LocatorCandidate(TrackedWaypoint waypoint, UUID uuid, float yaw, float dist, String name, String key) {
this.waypoint = waypoint;
this.uuid = uuid;
this.yaw = yaw;
this.dist = dist;
this.name = name != null ? name : "";
this.nameSort = this.name.toLowerCase(Locale.ROOT);
this.key = key;
this.keySort = key != null ? key : "";
}
}
record LocatorHead(float x, float y, float size, float radius, AbstractClientPlayer player,
Identifier skin, RenderColor color, RenderColor outlineColor, float outlineThickness,
boolean target) {
}
}

View file

@ -0,0 +1,66 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.hud.nondraggable.impl;
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.CustomBarModel.BarColors;
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.util.Mth;
import static dev.loki.lovisual.features.gui.hud.nondraggable.impl.CustomBar.*;
final class CustomJumpBarRenderer {
private CustomJumpBarRenderer() {
}
static boolean shouldUseJumpBar(Minecraft mc) {
if (mc == null || mc.player == null) return false;
var mount = mc.player.jumpableVehicle();
return mount != null && (mc.player.getJumpRidingScale() > 0.0f || mount.getJumpCooldown() > 0);
}
static void renderJumpBarBackground(Renderer2D r2d, Minecraft mc) {
if (mc == null || mc.player == null) return;
CustomBar bm = CustomBar.get();
if (bm == null || !bm.isHudBarEnabled() || !bm.isHotbarXpBarEnabled()) return;
float progress = mc.player.jumpableVehicle() != null && mc.player.jumpableVehicle().getJumpCooldown() <= 0
? Mth.clamp(mc.player.getJumpRidingScale(), 0.0f, 1.0f)
: 1.0f;
float scale = (float) mc.getWindow().getGuiScale();
float x = (mc.getWindow().getGuiScaledWidth() - XP_BAR_WIDTH) * 0.5f;
float y = mc.getWindow().getGuiScaledHeight() - 22f - XP_BAR_OFFSET - XP_BAR_HEIGHT;
CustomXpBarRenderer.drawVanillaGlassTrack(r2d, x, y, scale, progress, bm, false);
}
static void renderJumpBarTexture(GuiGraphicsExtractor ctx, Minecraft mc) {
if (ctx == null || mc == null || mc.player == null) return;
CustomBar bm = CustomBar.get();
if (bm == null || !bm.isHudBarEnabled() || !bm.isHotbarXpBarEnabled()) return;
var mount = mc.player.jumpableVehicle();
if (mount == null) return;
int x = Mth.floor((mc.getWindow().getGuiScaledWidth() - XP_BAR_WIDTH) * 0.5f);
int y = Mth.floor(mc.getWindow().getGuiScaledHeight() - 22f - XP_BAR_OFFSET - XP_BAR_HEIGHT);
BarColors colors = CustomXpBarRenderer.colors(bm);
ctx.blitSprite(LoVisualRenderPipelines.GUI_TEXTURE_LOOKUP, JUMP_BACKGROUND, x, y, 182, 5,
CustomXpBarRenderer.applyBarAlpha(colors.trackTexture(), bm));
if (mount.getJumpCooldown() > 0) {
ctx.blitSprite(LoVisualRenderPipelines.GUI_TEXTURE_LOOKUP, JUMP_COOLDOWN, x, y, 182, 5,
CustomXpBarRenderer.applyBarAlpha(colors.fillTexture(), bm));
return;
}
int progress = Mth.lerpDiscrete(mc.player.getJumpRidingScale(), 0, 182);
if (progress > 0) {
ctx.blitSprite(LoVisualRenderPipelines.GUI_TEXTURE_LOOKUP, JUMP_PROGRESS, 182, 5, 0, 0, x, y, progress, 5,
CustomXpBarRenderer.applyBarAlpha(colors.fillTexture(), bm));
}
}
}

View file

@ -0,0 +1,108 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.hud.nondraggable.impl;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.client.waypoints.ClientWaypointManager;
import net.minecraft.util.Mth;
import net.minecraft.world.waypoints.PartialTickSupplier;
import net.minecraft.world.waypoints.TrackedWaypoint;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
final class CustomLocatorCollector {
private CustomLocatorCollector() {
}
static List<CustomBarModel.LocatorCandidate> collect(Minecraft mc, CustomBar bm, TrackedWaypoint.Camera yawProvider,
PartialTickSupplier tickProgress, Set<UUID> mapPlayers,
ClientWaypointManager handler) {
List<CustomBarModel.LocatorCandidate> candidates = new ArrayList<>();
handler.forEachWaypoint(mc.player, waypoint -> {
if (isSourcePlayer(mc.player, waypoint)) return;
UUID wpUuid = waypoint.id().left().orElse(null);
if (wpUuid != null && !mapPlayers.isEmpty() && mapPlayers.contains(wpUuid)) return;
double yaw = waypoint.yawAngleToCamera(mc.level, yawProvider, tickProgress);
if (yaw <= -CustomBar.LOCATOR_ANGLE_RANGE || yaw >= CustomBar.LOCATOR_ANGLE_RANGE) return;
float dist = Mth.sqrt((float) waypoint.distanceSquared(mc.player));
String name = CustomLocatorResolver.getWaypointName(mc, waypoint, wpUuid);
String key = locatorLabelKey(waypoint, wpUuid);
candidates.add(new CustomBarModel.LocatorCandidate(
waypoint,
wpUuid,
(float) yaw,
dist,
name,
key
));
});
return candidates;
}
static boolean isSourcePlayer(LocalPlayer player, TrackedWaypoint waypoint) {
if (player == null || waypoint == null) return false;
return waypoint.id().left()
.map(uuid -> uuid.equals(player.getUUID()))
.orElse(false);
}
static String locatorLabelKey(TrackedWaypoint waypoint, UUID uuid) {
if (uuid != null) return uuid.toString();
return waypoint.id().right().orElse(null);
}
static final class Pass {
final GuiGraphicsExtractor ctx;
final Minecraft mc;
final CustomBar bm;
final PartialTickSupplier tickProgress;
final float barY;
final int startX;
final boolean drawArrows;
final boolean drawHeads;
final boolean labelsEnabled;
final CustomBarModel.LocatorLabelMode labelMode;
final float labelAngle;
final float dt;
final Set<String> seenLabels;
final List<CustomBarModel.LocatorLabel> labels;
final List<CustomBarModel.LocatorHead> heads;
Pass(GuiGraphicsExtractor ctx, Minecraft mc, CustomBar bm, PartialTickSupplier tickProgress,
float barY, int startX, boolean drawArrows, boolean drawHeads,
boolean labelsEnabled, CustomBarModel.LocatorLabelMode labelMode, float labelAngle, float dt,
Set<String> seenLabels, List<CustomBarModel.LocatorLabel> labels, List<CustomBarModel.LocatorHead> heads) {
this.ctx = ctx;
this.mc = mc;
this.bm = bm;
this.tickProgress = tickProgress;
this.barY = barY;
this.startX = startX;
this.drawArrows = drawArrows;
this.drawHeads = drawHeads;
this.labelsEnabled = labelsEnabled;
this.labelMode = labelMode;
this.labelAngle = labelAngle;
this.dt = dt;
this.seenLabels = seenLabels;
this.labels = labels;
this.heads = heads;
}
}
}

View file

@ -0,0 +1,75 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.hud.nondraggable.impl;
import dev.loki.lovisual.features.gui.hud.HudRenderUtil;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.helpers.PlayerHeadRenderer;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import java.util.List;
final class CustomLocatorHeads {
private CustomLocatorHeads() {
}
static void render(GuiGraphicsExtractor ctx, CustomBar bm, List<CustomBarModel.LocatorHead> heads, float scale) {
boolean startedHeadsBatch = CustomLocatorRenderer.beginOwnedColorBatch();
try {
for (int i = heads.size() - 1; i >= 0; i--) {
CustomBarModel.LocatorHead head = heads.get(i);
float headX = head.x() * scale;
float headYScaled = head.y() * scale;
float headSize = head.size() * scale;
float headRadius = head.radius() * scale;
float headOutline = head.outlineThickness() * scale;
if (head.target()) {
int glow = CustomXpBarRenderer.applyBarAlpha(
HudRenderUtil.scaleAlpha(0xFF000000 | bm.getLocatorSearchColorRgb(), 0.28f), bm);
Renderer2D.COLOR.roundedRect(
headX - 1.9f * scale,
headYScaled - 1.9f * scale,
headSize + 3.8f * scale,
headSize + 3.8f * scale,
headRadius + 2.4f * scale,
scale,
glow
);
}
if (head.player() != null) {
PlayerHeadRenderer.drawRounded(
ctx,
headX, headYScaled, headSize,
headRadius,
head.player(),
head.color(),
true,
head.outlineColor(),
headOutline,
false
);
} else if (head.skin() != null) {
PlayerHeadRenderer.drawRounded(
ctx,
headX, headYScaled, headSize,
headRadius,
head.skin(),
head.color(),
true,
head.outlineColor(),
headOutline,
false
);
}
}
} finally {
CustomLocatorRenderer.renderOwnedColorBatch(startedHeadsBatch);
}
}
}

View file

@ -0,0 +1,65 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.hud.nondraggable.impl;
import dev.loki.lovisual.render.engine.animation.AnimationUtility;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static dev.loki.lovisual.features.gui.hud.nondraggable.impl.CustomBar.*;
final class CustomLocatorLabelCache {
private static final Map<String, Float> ANIM = new HashMap<>();
private static final Map<String, CustomBarModel.LocatorLabel> CACHE = new HashMap<>();
private CustomLocatorLabelCache() {
}
static float updateAnim(String key, float target, float dt) {
float current = ANIM.getOrDefault(key, 0f);
float next = AnimationUtility.approach(current, target, dt, LOCATOR_LABEL_ANIM_SPEED);
next = AnimationUtility.snap(next, target, 0.01f);
ANIM.put(key, next);
return next;
}
static void cache(String key, CustomBarModel.LocatorLabel label) {
CACHE.put(key, label);
}
static void decay(Set<String> seen, float dt) {
if (ANIM.isEmpty()) return;
var it = ANIM.entrySet().iterator();
while (it.hasNext()) {
var entry = it.next();
if (seen.contains(entry.getKey())) continue;
float next = AnimationUtility.approach(entry.getValue(), 0f, dt, LOCATOR_LABEL_ANIM_SPEED);
next = AnimationUtility.snap(next, 0f, 0.01f);
if (next <= 0.0f) {
CACHE.remove(entry.getKey());
it.remove();
} else {
entry.setValue(next);
}
}
}
static void appendCached(List<CustomBarModel.LocatorLabel> labels) {
if (labels == null || CACHE.isEmpty()) return;
for (Map.Entry<String, CustomBarModel.LocatorLabel> entry : CACHE.entrySet()) {
float alpha = ANIM.getOrDefault(entry.getKey(), 0f);
if (alpha <= 0.01f) continue;
CustomBarModel.LocatorLabel cached = entry.getValue();
labels.add(new CustomBarModel.LocatorLabel(cached.name(), cached.distance(), alpha, cached.target(), cached.active(), cached.sort()));
}
}
}

View file

@ -0,0 +1,102 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.hud.nondraggable.impl;
import dev.loki.lovisual.render.engine.math.ColorMath;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import net.minecraft.util.Mth;
import static dev.loki.lovisual.features.gui.hud.nondraggable.impl.CustomBar.*;
import static dev.loki.lovisual.features.theme.Theme.theme;
final class CustomLocatorLabelPanel {
private CustomLocatorLabelPanel() {
}
static void drawPanel(Renderer2D r2d, CustomBar bm, float x, float y, float w, float h,
float radius, float scale, float alpha, boolean hasTarget) {
if (r2d == null || alpha <= 0.001f || w <= 0f || h <= 0f) return;
float effectiveAlpha = Mth.clamp(alpha * (bm != null ? bm.getHudOverlayAlphaFactor() : 1.0f), 0.0f, 1.0f);
float materialAlpha = Mth.clamp(0.78f + effectiveAlpha * 0.22f, 0.0f, 1.0f);
float blurAlpha = effectiveAlpha;
float thickness = 15.5f * Math.max(0.001f, scale);
r2d.liquidGlassRect(
x,
y,
w,
h,
radius,
thickness,
0xFFFFFFFF,
materialAlpha,
blurAlpha,
-26.0f,
0.96f,
0.98f,
0.46f,
0.172f * materialAlpha,
2.65f
);
r2d.roundedRect(
x,
y,
w,
h,
radius,
scale,
CustomXpBarRenderer.applyBarAlpha(ColorMath.scaleAlpha(theme().windowBg(), alpha * 0.22f), bm)
);
r2d.roundedRectStrokeGradient(
x,
y,
w,
h,
radius,
Math.max(0.45f, 0.55f * scale),
CustomXpBarRenderer.applyBarAlpha(ColorMath.scaleAlpha(theme().textPrimary(), alpha * 0.18f), bm),
CustomXpBarRenderer.applyBarAlpha(ColorMath.scaleAlpha(theme().strokeSoft(), alpha * 0.20f), bm),
118f
);
}
static void drawRow(Renderer2D r2d, CustomBar bm, CustomBarModel.LocatorLabel label,
float x, float y, float w, float h, float radius, float scale, float alpha) {
if (r2d == null || label == null || alpha <= 0.001f || w <= 0f || h <= 0f) return;
int rowFill = label.target()
? CustomXpBarRenderer.applyBarAlpha(ColorMath.scaleAlpha(theme().textPrimary(), alpha * 0.105f), bm)
: CustomXpBarRenderer.applyBarAlpha(ColorMath.scaleAlpha(theme().surfaceHover(), alpha * 0.055f), bm);
r2d.roundedRect(
x,
y,
w,
h,
radius,
scale,
rowFill
);
if (label.target()) {
float markerW = LOCATOR_LABEL_ACCENT_W * scale;
float markerH = Math.max(1.0f * scale, h - 4.2f * scale);
r2d.roundedRect(
x + 1.8f * scale,
y + (h - markerH) * 0.5f,
markerW,
markerH,
markerW * 0.5f,
0f,
CustomXpBarRenderer.applyBarAlpha(ColorMath.scaleAlpha(theme().textPrimary(), alpha * 0.72f), bm)
);
}
}
}

View file

@ -0,0 +1,130 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.hud.nondraggable.impl;
import dev.loki.lovisual.render.engine.animation.AnimationUtility;
import dev.loki.lovisual.render.engine.color.RenderColor;
import dev.loki.lovisual.render.engine.math.ColorMath;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.text.Fonts;
import dev.loki.lovisual.render.engine.text.TextRenderer;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.util.Mth;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import static dev.loki.lovisual.features.gui.hud.nondraggable.impl.CustomBar.*;
import static dev.loki.lovisual.features.theme.Theme.theme;
final class CustomLocatorLabels {
private CustomLocatorLabels() {
}
static void render(GuiGraphicsExtractor ctx, CustomBar bm, float screenW, float barY, float scale,
List<CustomBarModel.LocatorLabel> labels) {
TextRenderer tr = Fonts.renderer("Iosevka");
Renderer2D r2d = Renderer2D.COLOR;
float labelScale = LOCATOR_LABEL_SCALE * scale;
List<CustomBarModel.LocatorLabel> layoutLabels = new ArrayList<>();
for (CustomBarModel.LocatorLabel label : labels) {
if (label.active()) {
layoutLabels.add(label);
}
}
if (layoutLabels.isEmpty()) {
layoutLabels.addAll(labels);
}
layoutLabels.sort(Comparator
.comparing((CustomBarModel.LocatorLabel label) -> !label.target())
.thenComparing(label -> label.sort() == null ? "" : label.sort())
.thenComparing(CustomBarModel.LocatorLabel::name));
List<CustomBarModel.LocatorLabel> draw = layoutLabels.size() > LOCATOR_LABEL_MAX_ROWS
? new ArrayList<>(layoutLabels.subList(0, LOCATOR_LABEL_MAX_ROWS))
: layoutLabels;
tr.begin(labelScale, false, false);
float textH = (float) tr.getHeight(true);
float markerReserve = draw.stream().anyMatch(CustomBarModel.LocatorLabel::target) ? 4.7f * scale : 0.0f;
float chipPadX = LOCATOR_LABEL_CHIP_PAD_X * scale;
float chipPadY = LOCATOR_LABEL_CHIP_PAD_Y * scale;
float distanceGap = 5.6f * scale;
float contentW = 0.0f;
for (CustomBarModel.LocatorLabel label : draw) {
float nameW = (float) tr.getWidth(label.name(), true);
float distanceW = label.distance().isEmpty() ? 0.0f : (float) tr.getWidth(label.distance(), true);
float rowTextW = nameW + (distanceW > 0.0f ? distanceGap + distanceW : 0.0f);
contentW = Math.max(contentW, markerReserve + rowTextW);
}
tr.end();
float padX = LOCATOR_LABEL_PADDING_X * scale;
float padY = LOCATOR_LABEL_PADDING_Y * scale;
float rowGap = LOCATOR_LABEL_ROW_GAP * scale;
float chipH = textH + chipPadY * 2.0f;
float rowStep = chipH + rowGap;
float rowW = Math.max(58.0f * scale, contentW + chipPadX * 2.0f);
float panelW = padX * 2.0f + rowW;
float panelH = padY * 2.0f + draw.size() * chipH + Math.max(0, draw.size() - 1) * rowGap;
float panelX = (screenW * scale - panelW) * 0.5f;
panelX = Mth.clamp(panelX, 4.0f * scale, screenW * scale - panelW - 4.0f * scale);
float panelY = barY * scale - LOCATOR_LABEL_OFFSET * scale - panelH;
panelY = Math.max(4.0f * scale, panelY);
float panelAlpha = 0.0f;
boolean hasTarget = false;
for (CustomBarModel.LocatorLabel label : draw) {
panelAlpha = Math.max(panelAlpha, Mth.clamp(label.alpha(), 0f, 1f));
hasTarget |= label.target();
}
boolean startedLabelBatch = CustomLocatorRenderer.beginOwnedColorBatch();
try {
float ease = locatorLabelEase(panelAlpha);
float radius = LOCATOR_LABEL_RADIUS * scale;
float fadedPanelY = panelY + (1.0f - ease) * 2.2f * scale;
CustomLocatorLabelPanel.drawPanel(r2d, bm, panelX, fadedPanelY, panelW, panelH, radius, scale, ease, hasTarget);
float rowX = panelX + padX;
float rowY = fadedPanelY + padY;
float chipR = Math.min(chipH * 0.48f, LOCATOR_LABEL_CHIP_RADIUS * scale);
for (int row = 0; row < draw.size(); row++) {
CustomBarModel.LocatorLabel label = draw.get(row);
float anim = Mth.clamp(label.alpha(), 0f, 1f);
CustomLocatorLabelPanel.drawRow(r2d, bm, label, rowX, rowY + row * rowStep, rowW, chipH, chipR, scale, anim);
}
tr.begin(labelScale, false, false);
for (int row = 0; row < draw.size(); row++) {
CustomBarModel.LocatorLabel label = draw.get(row);
float anim = Mth.clamp(label.alpha(), 0f, 1f);
float textBaseX = rowX + chipPadX + markerReserve;
float textY = rowY + row * rowStep + (chipH - textH) * 0.5f;
int nameArgb = CustomXpBarRenderer.applyBarAlpha(
ColorMath.scaleAlpha(theme().textPrimary(), anim * (label.target() ? 1.0f : 0.92f)), bm);
tr.render(label.name(), textBaseX, textY, new RenderColor(nameArgb), true);
if (!label.distance().isEmpty()) {
float distanceW = (float) tr.getWidth(label.distance(), true);
float distanceX = rowX + rowW - chipPadX - distanceW;
int distanceArgb = CustomXpBarRenderer.applyBarAlpha(
ColorMath.scaleAlpha(theme().textPrimary(), anim * 0.90f), bm);
tr.render(label.distance(), distanceX, textY, new RenderColor(distanceArgb), true);
}
}
tr.end();
} finally {
CustomLocatorRenderer.renderOwnedColorBatch(startedLabelBatch);
}
}
private static float locatorLabelEase(float alpha) {
return AnimationUtility.easeOutCubic(alpha);
}
}

View file

@ -0,0 +1,150 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.hud.nondraggable.impl;
import dev.loki.lovisual.render.engine.color.RenderColor;
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.resources.Identifier;
import net.minecraft.util.Mth;
import net.minecraft.world.waypoints.TrackedWaypoint;
import java.util.Locale;
import java.util.UUID;
import static dev.loki.lovisual.features.gui.hud.nondraggable.impl.CustomBar.*;
final class CustomLocatorMarkers {
private CustomLocatorMarkers() {
}
static void process(CustomLocatorCollector.Pass pass, CustomBarModel.LocatorCandidate candidate) {
CustomBar bm = pass.bm;
Minecraft mc = pass.mc;
float yaw = candidate.yaw;
int offset = Mth.floor(yaw * (LOCATOR_HEAD_RANGE / 2f) / LOCATOR_ANGLE_RANGE);
float drawX = pass.startX + offset;
float dist = candidate.dist;
UUID wpUuid = candidate.uuid;
boolean isTarget = CustomLocatorResolver.isLocatorTarget(wpUuid, bm);
boolean isDimmed = CustomLocatorResolver.isLocatorDimmed(wpUuid, bm);
float headScale = locatorHeadScale(dist, isTarget, isDimmed, bm);
float headSize = LOCATOR_HEAD_SIZE * headScale;
float headRadius = LOCATOR_HEAD_RADIUS * headScale;
float headY = pass.barY + (LOCATOR_BAR_HEIGHT - headSize) * 0.5f + LOCATOR_HEAD_GAP;
int baseColor = CustomLocatorResolver.resolveWaypointColor(candidate.waypoint);
RenderColor headColor = new RenderColor(CustomXpBarRenderer.applyBarAlpha(0xFFFFFFFF, bm));
RenderColor outlineColor = isTarget
? new RenderColor(CustomXpBarRenderer.applyBarAlpha(0xFF000000 | bm.getLocatorSearchColorRgb(), bm))
: locatorOutlineColor(baseColor, dist, bm);
float outlineThickness = locatorOutlineThickness(dist) * headScale;
if (isTarget) {
outlineThickness = Math.max(outlineThickness, 1.85f * headScale);
}
AbstractClientPlayer player = CustomLocatorResolver.resolveWaypointPlayer(mc, wpUuid);
Identifier skin = (player == null) ? CustomLocatorResolver.resolveWaypointSkin(mc, wpUuid, candidate.waypoint) : null;
if (pass.drawHeads) {
pass.heads.add(new CustomBarModel.LocatorHead(
drawX, headY, headSize, headRadius,
player, skin, headColor, outlineColor, outlineThickness, isTarget
));
}
TrackedWaypoint.PitchDirection pitch = candidate.waypoint.pitchDirectionToCamera(mc.level, mc.gameRenderer, pass.tickProgress);
if (pass.drawArrows && (pitch == TrackedWaypoint.PitchDirection.UP || pitch == TrackedWaypoint.PitchDirection.DOWN)) {
Identifier arrow = (pitch == TrackedWaypoint.PitchDirection.DOWN) ? LOCATOR_ARROW_DOWN : LOCATOR_ARROW_UP;
int xOffset = Mth.floor(drawX + (headSize - LOCATOR_ARROW_W) * 0.5f);
int yOffset = (pitch == TrackedWaypoint.PitchDirection.DOWN)
? Mth.floor(headY + headSize + LOCATOR_ARROW_GAP)
: Mth.floor(headY - LOCATOR_ARROW_H - LOCATOR_ARROW_GAP);
pass.ctx.blitSprite(
LoVisualRenderPipelines.GUI_TEXTURE_LOOKUP,
arrow,
xOffset,
yOffset,
LOCATOR_ARROW_W,
LOCATOR_ARROW_H,
CustomXpBarRenderer.applyBarAlpha(CustomXpBarRenderer.colors(bm).locatorTexture(), bm)
);
}
if (!pass.labelsEnabled || pass.labels == null || pass.seenLabels == null) return;
String labelKey = candidate.key != null ? candidate.key : candidate.nameSort;
boolean showLabel = shouldShowLocatorLabel(pass.labelMode, yaw, pass.labelAngle, isTarget);
if (showLabel && labelKey != null && !labelKey.isBlank()) {
pass.seenLabels.add(labelKey);
}
float anim = labelKey != null && !labelKey.isBlank()
? CustomLocatorLabelCache.updateAnim(labelKey, showLabel ? 1f : 0f, pass.dt)
: (showLabel ? 1f : 0f);
if (anim <= 0.01f) return;
String name = candidate.name;
if (name == null || name.isBlank()) return;
String distanceText = "";
if (bm.isLocatorLabelDistanceEnabled()) {
distanceText = formatDistance(dist);
}
CustomBarModel.LocatorLabel label = new CustomBarModel.LocatorLabel(name, distanceText, anim, isTarget, showLabel, candidate.nameSort);
pass.labels.add(label);
if (labelKey != null && !labelKey.isBlank()) {
CustomLocatorLabelCache.cache(labelKey, label);
}
}
private static float locatorHeadScale(float dist, boolean isTarget, boolean isDimmed, CustomBar bm) {
if (isTarget) return 1.0f;
boolean dimmedOnly = bm != null && bm.isLocatorDimmedScaleOnly();
if (dimmedOnly && !isDimmed) return 1.0f;
float range = LOCATOR_HEAD_SCALE_END - LOCATOR_HEAD_SCALE_START;
if (range <= 0.0f) return 1.0f;
float t = Mth.clamp((dist - LOCATOR_HEAD_SCALE_START) / range, 0f, 1f);
return Mth.lerp(t, 1.0f, LOCATOR_HEAD_MIN_SCALE);
}
private static RenderColor locatorOutlineColor(int baseArgb, float dist, CustomBar bm) {
float t = Mth.clamp(dist / LOCATOR_OUTLINE_DISTANCE, 0f, 1f);
int baseRgb = baseArgb & 0x00FFFFFF;
int rgb = dev.loki.lovisual.render.engine.math.ColorMath.mixRgb(baseRgb, 0x000000, 0.25f + 0.35f * t);
int alpha = Math.round(Mth.lerp(230f, 120f, t));
int argb = CustomXpBarRenderer.applyBarAlpha((alpha << 24) | rgb, bm);
return new RenderColor(argb);
}
private static float locatorOutlineThickness(float dist) {
float t = Mth.clamp(dist / LOCATOR_OUTLINE_DISTANCE, 0f, 1f);
return Mth.lerp(LOCATOR_OUTLINE_MAX, LOCATOR_OUTLINE_MIN, t);
}
private static boolean shouldShowLocatorLabel(CustomBarModel.LocatorLabelMode mode, float yaw, float angle, boolean isTarget) {
float abs = Math.abs(yaw);
return switch (mode) {
case ALWAYS -> true;
case CENTER -> abs <= angle;
case TARGETS -> isTarget;
case CENTER_TARGETS -> isTarget || abs <= angle;
};
}
private static String formatDistance(float dist) {
if (!Float.isFinite(dist)) return "";
return String.format(Locale.ROOT, "%.0fm", dist);
}
}

View file

@ -0,0 +1,170 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.hud.nondraggable.impl;
import dev.loki.lovisual.render.engine.animation.AnimationUtility;
import dev.loki.lovisual.render.engine.core.ViewportContext;
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.util.map.MapPlayersCache;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.waypoints.ClientWaypointManager;
import net.minecraft.util.Mth;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.waypoints.PartialTickSupplier;
import net.minecraft.world.waypoints.TrackedWaypoint;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
final class CustomLocatorRenderer {
private CustomLocatorRenderer() {
}
static void renderLocatorBackground(Renderer2D r2d, Minecraft mc) {
if (mc == null || mc.player == null) return;
CustomBar bm = CustomBar.get();
if (bm == null || !bm.isHudBarEnabled() || !bm.isHotbarXpBarEnabled()) return;
float scale = (float) mc.getWindow().getGuiScale();
float screenW = mc.getWindow().getGuiScaledWidth();
float screenH = mc.getWindow().getGuiScaledHeight();
float baseY = screenH - 22f;
float x = (screenW - CustomBar.XP_BAR_WIDTH) * 0.5f;
float y = baseY - CustomBar.XP_BAR_OFFSET - CustomBar.LOCATOR_BAR_HEIGHT;
CustomXpBarRenderer.drawVanillaGlassTrack(r2d, x, y, scale, 1.0f, bm, true);
}
static void renderLocatorTexture(GuiGraphicsExtractor ctx, Minecraft mc) {
if (ctx == null || mc == null || mc.player == null) return;
CustomBar bm = CustomBar.get();
if (bm == null || !bm.isHudBarEnabled() || !bm.isHotbarXpBarEnabled()) return;
int x = Mth.floor((mc.getWindow().getGuiScaledWidth() - CustomBar.XP_BAR_WIDTH) * 0.5f);
int y = Mth.floor(mc.getWindow().getGuiScaledHeight() - 22f - CustomBar.XP_BAR_OFFSET - CustomBar.LOCATOR_BAR_HEIGHT);
ctx.blitSprite(LoVisualRenderPipelines.GUI_TEXTURE_LOOKUP, CustomBar.LOCATOR_BACKGROUND, x, y, 182, 5,
CustomXpBarRenderer.applyBarAlpha(CustomXpBarRenderer.colors(bm).locatorTexture(), bm));
}
static void renderLocatorArrows(GuiGraphicsExtractor ctx, DeltaTracker tickCounter, Minecraft mc) {
renderLocatorMarkers(ctx, tickCounter, mc, true, false);
}
static void renderLocatorHeads(GuiGraphicsExtractor ctx, DeltaTracker tickCounter, Minecraft mc) {
renderLocatorMarkers(ctx, tickCounter, mc, false, true);
}
static void renderLocatorMarkers(GuiGraphicsExtractor ctx, DeltaTracker tickCounter, Minecraft mc) {
renderLocatorMarkers(ctx, tickCounter, mc, true, true);
}
static void renderLocatorMarkers(GuiGraphicsExtractor ctx, DeltaTracker tickCounter, Minecraft mc,
boolean drawArrows, boolean drawHeads) {
if (ctx == null || mc == null || mc.player == null || mc.level == null) return;
CustomBar bm = CustomBar.get();
if (bm == null || !bm.isHudBarEnabled() || !bm.isHotbarXpBarEnabled()) return;
if (mc.getConnection() == null) return;
ClientWaypointManager handler = mc.getConnection().getWaypointManager();
if (handler == null || !handler.hasWaypoints()) return;
float screenW = mc.getWindow().getGuiScaledWidth();
float screenH = mc.getWindow().getGuiScaledHeight();
float baseY = screenH - 22f;
float barX = (screenW - CustomBar.XP_BAR_WIDTH) * 0.5f;
float barY = baseY - CustomBar.XP_BAR_OFFSET - CustomBar.LOCATOR_BAR_HEIGHT;
int startX = Mth.floor(barX + CustomBar.LOCATOR_HEAD_RANGE * 0.5f);
PartialTickSupplier tickProgress = entity -> tickCounter.getGameTimeDeltaPartialTick(false);
var camera = mc.gameRenderer.mainCamera();
TrackedWaypoint.Camera yawProvider = new TrackedWaypoint.Camera() {
@Override
public float yaw() {
return camera.yRot();
}
@Override
public Vec3 position() {
return camera.position();
}
};
Set<UUID> mapPlayers = MapPlayersCache.getPlayers(mc, bm);
boolean labelsEnabled = drawHeads && bm.isLocatorLabelsEnabled();
CustomBarModel.LocatorLabelMode labelMode = labelsEnabled
? bm.getLocatorLabelMode()
: CustomBarModel.LocatorLabelMode.ALWAYS;
float labelAngle = labelsEnabled ? bm.getLocatorLabelAngle() : 0f;
float dt = drawHeads ? AnimationUtility.deltaTime() : 0.0f;
List<CustomBarModel.LocatorCandidate> candidates =
CustomLocatorCollector.collect(mc, bm, yawProvider, tickProgress, mapPlayers, handler);
if (candidates.isEmpty() && !drawHeads) return;
candidates.sort((a, b) -> {
int cmp = Float.compare(a.dist, b.dist);
if (cmp != 0) return cmp;
cmp = a.nameSort.compareTo(b.nameSort);
if (cmp != 0) return cmp;
return a.keySort.compareTo(b.keySort);
});
List<CustomBarModel.LocatorHead> heads = new ArrayList<>();
Set<String> seenLabels = labelsEnabled ? new HashSet<>() : null;
List<CustomBarModel.LocatorLabel> labels = labelsEnabled ? new ArrayList<>() : null;
CustomLocatorCollector.Pass pass = new CustomLocatorCollector.Pass(
ctx, mc, bm, tickProgress, barY, startX, drawArrows, drawHeads,
labelsEnabled, labelMode, labelAngle, dt, seenLabels, labels, heads);
for (CustomBarModel.LocatorCandidate candidate : candidates) {
CustomLocatorMarkers.process(pass, candidate);
}
if (!drawHeads) return;
if (labelsEnabled && labels != null && seenLabels != null) {
CustomLocatorLabelCache.decay(seenLabels, dt);
labels.clear();
CustomLocatorLabelCache.appendCached(labels);
}
if (heads.isEmpty() && (labels == null || labels.isEmpty())) return;
float scale = (float) mc.getWindow().getGuiScale();
ViewportContext.beginUnscaled(ctx);
try {
if (!heads.isEmpty()) {
CustomLocatorHeads.render(ctx, bm, heads, scale);
}
if (labelsEnabled && labels != null && !labels.isEmpty()) {
CustomLocatorLabels.render(ctx, bm, screenW, barY, scale, labels);
}
} finally {
ViewportContext.end(ctx);
}
}
static boolean beginOwnedColorBatch() {
if (Renderer2D.UI_BATCHER.isActive()) {
return false;
}
Renderer2D.COLOR.begin();
return true;
}
static void renderOwnedColorBatch(boolean owned) {
if (owned) {
Renderer2D.COLOR.render();
}
}
}

View file

@ -0,0 +1,167 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.hud.nondraggable.impl;
import com.mojang.authlib.GameProfile;
import dev.loki.lovisual.util.player.PlayerSkinResolver;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.PlayerInfo;
import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.resources.Identifier;
import net.minecraft.util.Mth;
import net.minecraft.world.waypoints.TrackedWaypoint;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import java.util.UUID;
final class CustomLocatorResolver {
private CustomLocatorResolver() {
}
static int resolveWaypointColor(TrackedWaypoint waypoint) {
CustomBar bm = CustomBar.get();
UUID wpUuid = waypoint.id().left().orElse(null);
boolean isPlayerWaypoint = wpUuid != null;
if (bm != null && bm.isLocatorSearchEnabled() && isPlayerWaypoint) {
Set<String> targets = bm.getLocatorSearchPlayers();
if (targets != null && !targets.isEmpty()) {
if (isPlayerInTargets(wpUuid, targets)) {
return 0xFF000000 | bm.getLocatorSearchColorRgb();
}
return 0xFF000000 | bm.getLocatorDimmedColorRgb();
}
}
var cfg = waypoint.icon();
if (cfg != null && cfg.color.isPresent()) {
return 0xFF000000 | (cfg.color.get() & 0x00FFFFFF);
}
int rgb = waypoint.id()
.map(CustomLocatorResolver::colorFromUuid, CustomLocatorResolver::colorFromName);
return 0xFF000000 | (rgb & 0x00FFFFFF);
}
static AbstractClientPlayer resolveWaypointPlayer(Minecraft mc, UUID uuid) {
if (mc == null || mc.level == null || uuid == null) return null;
var player = mc.level.getPlayerByUUID(uuid);
return player instanceof AbstractClientPlayer clientPlayer ? clientPlayer : null;
}
static Identifier resolveWaypointSkin(Minecraft mc, UUID uuid, TrackedWaypoint waypoint) {
GameProfile profile = null;
if (mc != null && mc.getConnection() != null && uuid != null) {
PlayerInfo entry = mc.getConnection().getPlayerInfo(uuid);
if (entry != null && entry.getProfile() != null) {
profile = entry.getProfile();
}
}
if (profile == null && uuid != null) {
profile = new GameProfile(uuid, uuid.toString());
}
if (profile == null) {
String name = waypoint.id().right().orElse(null);
if (name != null && !name.isBlank()) {
profile = new GameProfile(offlineUuid(name), name);
}
}
return PlayerSkinResolver.resolveProfileSkin(profile);
}
static String getWaypointName(Minecraft mc, TrackedWaypoint waypoint, UUID uuid) {
if (mc != null && mc.getConnection() != null && uuid != null) {
PlayerInfo entry = mc.getConnection().getPlayerInfo(uuid);
if (entry != null && entry.getProfile() != null) {
String name = entry.getProfile().name();
if (name != null && !name.isBlank()) return name;
}
}
String fallback = waypoint.id().right().orElse(null);
if (fallback != null && !fallback.isBlank()) return fallback;
return uuid != null ? uuid.toString() : null;
}
static boolean isLocatorTarget(UUID wpUuid, CustomBar bm) {
if (bm == null || wpUuid == null || !bm.isLocatorSearchEnabled()) return false;
Set<String> targets = bm.getLocatorSearchPlayers();
if (targets == null || targets.isEmpty()) return false;
return isPlayerInTargets(wpUuid, targets);
}
static boolean isLocatorDimmed(UUID wpUuid, CustomBar bm) {
if (bm == null || wpUuid == null || !bm.isLocatorSearchEnabled()) return false;
Set<String> targets = bm.getLocatorSearchPlayers();
if (targets == null || targets.isEmpty()) return false;
return !isPlayerInTargets(wpUuid, targets);
}
static UUID offlineUuid(String name) {
if (name == null || name.isBlank()) return new UUID(0L, 0L);
String key = "OfflinePlayer:" + name;
return UUID.nameUUIDFromBytes(key.getBytes(StandardCharsets.UTF_8));
}
static boolean isPlayerInTargets(UUID wpUuid, Set<String> targets) {
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.getConnection() == null) return false;
for (String raw : targets) {
if (raw == null) continue;
String t = raw.trim();
if (t.isEmpty()) continue;
if (t.equalsIgnoreCase(wpUuid.toString())) {
return true;
}
PlayerInfo entry = findEntryByName(mc, t);
if (entry != null && entry.getProfile() != null) {
if (wpUuid.equals(entry.getProfile().id())) {
return true;
}
}
}
return false;
}
static PlayerInfo findEntryByName(Minecraft mc, String name) {
for (PlayerInfo e : mc.getConnection().getOnlinePlayers()) {
if (e.getProfile() != null
&& e.getProfile().name().equalsIgnoreCase(name)) {
return e;
}
}
return null;
}
static int colorFromUuid(UUID uuid) {
if (uuid == null) return 0xFFFFFFFF;
return colorFromHash(uuid.hashCode());
}
static int colorFromName(String name) {
if (name == null) return 0xFFFFFFFF;
return colorFromHash(name.hashCode());
}
static int colorFromHash(int hash) {
float hue = (hash & 0xFFFF) / 65535.0f;
int rgb = Mth.hsvToRgb(hue, 0.9f, 0.9f);
return rgb & 0x00FFFFFF;
}
}

View file

@ -0,0 +1,197 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.hud.nondraggable.impl;
import dev.loki.lovisual.features.gui.hud.HudRenderUtil;
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.CustomBarModel.BarColors;
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.CustomBarModel.XpTextInfo;
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.text.TextRenderer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.network.chat.Component;
import net.minecraft.util.Mth;
import static dev.loki.lovisual.features.gui.hud.nondraggable.impl.CustomBar.*;
import static dev.loki.lovisual.features.theme.Theme.theme;
final class CustomXpBarRenderer {
private CustomXpBarRenderer() {
}
static void renderXpBarBackground(Renderer2D r2d, Minecraft mc) {
if (mc == null || mc.player == null) return;
CustomBar bm = CustomBar.get();
if (bm == null || !bm.isHudBarEnabled() || !bm.isHotbarXpBarEnabled()) return;
float progress = Mth.clamp(mc.player.experienceProgress, 0.0f, 1.0f);
float scale = (float) mc.getWindow().getGuiScale();
float screenW = mc.getWindow().getGuiScaledWidth();
float screenH = mc.getWindow().getGuiScaledHeight();
float baseY = screenH - 22f;
float x = (screenW - XP_BAR_WIDTH) * 0.5f;
float y = baseY - XP_BAR_OFFSET - XP_BAR_HEIGHT;
drawVanillaGlassTrack(r2d, x, y, scale, progress, bm, false);
}
static void drawXpBar(Renderer2D r2d, float barX, float barY, float scale, float progress, CustomBar bm) {
if (r2d == null || bm == null) return;
drawVanillaGlassTrack(r2d, barX, barY, scale, progress, bm, false);
}
static void renderXpBarTexture(GuiGraphicsExtractor ctx, Minecraft mc) {
if (ctx == null || mc == null || mc.player == null) return;
CustomBar bm = CustomBar.get();
if (bm == null || !bm.isHudBarEnabled() || !bm.isHotbarXpBarEnabled()) return;
int k = mc.player.getXpNeededForNextLevel();
if (k <= 0) return;
int x = Mth.floor((mc.getWindow().getGuiScaledWidth() - XP_BAR_WIDTH) * 0.5f);
int y = Mth.floor(mc.getWindow().getGuiScaledHeight() - 22f - XP_BAR_OFFSET - XP_BAR_HEIGHT);
BarColors colors = colors(bm);
ctx.blitSprite(LoVisualRenderPipelines.GUI_TEXTURE_LOOKUP, XP_BACKGROUND, x, y, 182, 5,
applyBarAlpha(colors.trackTexture(), bm));
int progress = (int) (Mth.clamp(mc.player.experienceProgress, 0.0f, 1.0f) * 183.0f);
if (progress > 0) {
ctx.blitSprite(LoVisualRenderPipelines.GUI_TEXTURE_LOOKUP, XP_PROGRESS, 182, 5, 0, 0, x, y, progress, 5,
applyBarAlpha(colors.fillTexture(), bm));
}
}
static void drawVanillaGlassTrack(Renderer2D r2d, float barX, float barY, float scale,
float progress, CustomBar bm, boolean locator) {
if (r2d == null || bm == null) return;
float x = barX * scale;
float y = barY * scale;
float w = XP_BAR_WIDTH * scale;
float h = XP_BAR_HEIGHT * scale;
float radius = XP_BAR_RADIUS * scale;
BarColors colors = colors(bm);
HudRenderUtil.drawLiquidGlass(x, y, w, h, radius, scale, false, BAR_BLUR_ALPHA, BAR_GLASS_ALPHA);
int trackA = applyBarAlpha(colors.track(), bm);
int stroke = applyBarAlpha(colors.stroke(), bm);
r2d.roundedRectGradient(x, y, w, h, radius, BAR_SOFTNESS * scale, trackA,
HudRenderUtil.scaleAlpha(trackA, 0.76f), 0.0f);
float fillW = XP_BAR_WIDTH * Mth.clamp(progress, 0.0f, 1.0f) * scale;
if (fillW > 0.0f && !locator) {
float fillRadius = Math.min(radius, Math.min(fillW, h) * 0.5f);
r2d.roundedRectMaskedQuad(
x, y, fillW, h,
x, y, w, h,
radius, BAR_SOFTNESS * scale,
colors.fillStart(), colors.fillEnd(), colors.fillEnd(), colors.fillStart()
);
if (fillRadius > 0.0f) {
r2d.roundedRectGradient(x, y, fillW, Math.max(scale, h * 0.42f), fillRadius,
BAR_SOFTNESS * scale, HudRenderUtil.scaleAlpha(0xFFFFFFFF, 0.10f),
HudRenderUtil.scaleAlpha(0xFFFFFFFF, 0.02f), 90.0f);
}
}
if (LOCATOR_TICK_COUNT > 0 && locator) {
float lineW = Math.max(1.0f, 0.65f * scale);
float step = w / (LOCATOR_TICK_COUNT + 1f);
int tickColor = HudRenderUtil.scaleAlpha(colors.fillTexture(), 0.36f);
for (int i = 1; i <= LOCATOR_TICK_COUNT; i++) {
float lx = x + step * i - lineW * 0.5f;
r2d.quad(lx, y + scale, lineW, Math.max(scale, h - 2.0f * scale), tickColor);
}
}
r2d.roundedRectStroke(x, y, w, h, radius, BAR_SOFTNESS * scale, Math.max(0.65f, 0.72f * scale), stroke);
}
static BarColors colors(CustomBar bm) {
if (bm != null && bm.isCustomMode()) {
int track = HudRenderUtil.scaleAlpha(bm.customBgColor.getArgb(), BAR_TRACK_ALPHA);
int fillStart = HudRenderUtil.scaleAlpha(bm.customFillColor.getArgb(), BAR_FILL_ALPHA);
int fillEnd = HudRenderUtil.scaleAlpha(bm.customFillGradientColor.getArgb(), BAR_FILL_ALPHA);
int stroke = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(fillEnd, track, 0.38f), BAR_STROKE_ALPHA);
int textureTrack = forceOpaque(HudRenderUtil.mixColor(track, 0xFFFFFFFF, 0.18f));
int textureFill = forceOpaque(HudRenderUtil.mixColor(fillStart, fillEnd, 0.48f));
int locator = forceOpaque(HudRenderUtil.mixColor(fillEnd, track, 0.16f));
return new BarColors(track, fillStart, fillEnd, stroke, textureTrack, textureFill, locator);
}
int window = theme().windowBg();
int surface = theme().surface();
int accent = theme().accent();
int soft = theme().accentSoft();
int track = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(window, surface, 0.44f), BAR_TRACK_ALPHA);
int fillStart = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(soft, accent, 0.24f), BAR_FILL_ALPHA);
int fillEnd = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(accent, theme().textPrimary(), 0.18f), BAR_FILL_ALPHA);
int stroke = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(theme().windowStroke(), accent, 0.18f), BAR_STROKE_ALPHA);
int textureTrack = forceOpaque(HudRenderUtil.mixColor(track, theme().textMuted(), 0.22f));
int textureFill = forceOpaque(HudRenderUtil.mixColor(fillStart, fillEnd, 0.50f));
int locator = forceOpaque(HudRenderUtil.mixColor(accent, 0xFF1E7A4C, 0.42f));
return new BarColors(track, fillStart, fillEnd, stroke, textureTrack, textureFill, locator);
}
static int forceOpaque(int argb) {
return 0xFF000000 | (argb & 0x00FFFFFF);
}
static void renderXpBarText(GuiGraphicsExtractor ctx, Minecraft mc) {
if (mc == null || mc.player == null || ctx == null) return;
CustomBar bm = CustomBar.get();
if (bm == null || !bm.isHudBarEnabled() || !bm.isHotbarXpBarEnabled() || !bm.isHotbarXpBarShowLevel()) return;
if (mc.player.experienceLevel <= 0) return;
Component text = Component.translatable("gui.experience.level", mc.player.experienceLevel);
int x = (ctx.guiWidth() - mc.font.width(text)) / 2 + XP_TEXT_OFFSET;
int y = ctx.guiHeight() - 24 - 9 - 2 + XP_TEXT_OFFSET;
int color = applyBarAlpha(bm.getXpTextArgb(), bm);
ctx.text(mc.font, text, x + 1, y, 0xFF000000, false);
ctx.text(mc.font, text, x - 1, y, 0xFF000000, false);
ctx.text(mc.font, text, x, y + 1, 0xFF000000, false);
ctx.text(mc.font, text, x, y - 1, 0xFF000000, false);
ctx.text(mc.font, text, x, y, color, false);
}
static void renderPreview(Renderer2D r2d, float x, float y, float scale, float progress) {
CustomBar bm = CustomBar.get();
if (bm == null) return;
drawXpBar(r2d, x, y, scale, progress, bm);
}
static XpTextInfo buildPreviewText(float barX, float barY, float scale, int level) {
CustomBar bm = CustomBar.get();
if (bm == null) return new XpTextInfo("", 0, 0, 0, (float) XP_TEXT_SCALE * scale);
if (level <= 0) return new XpTextInfo("", 0, 0, 0, bm.getXpTextScale() * scale);
String text = String.valueOf(level);
TextRenderer tr = TextRenderer.get();
float textScale = bm.getXpTextScale() * scale;
tr.begin(textScale, false, false);
int textW = (int) Math.ceil(tr.getWidth(text, true));
int textH = (int) Math.ceil(tr.getHeight(true));
tr.end();
float barW = XP_BAR_WIDTH * scale;
int tx = Math.round(barX * scale + (barW - textW) * 0.5f) + Math.round(XP_TEXT_OFFSET * scale);
int ty = Math.round(barY * scale - textH - XP_TEXT_OFFSET * scale) + Math.round(XP_TEXT_OFFSET * scale);
int color = applyBarAlpha(bm.getXpTextArgb(), bm);
return new XpTextInfo(text, tx, ty, color, textScale);
}
static int applyBarAlpha(int argb, CustomBar bm) {
if (bm == null) return argb;
int a = (argb >>> 24) & 0xFF;
int na = (int) (a * bm.getHudOverlayAlphaFactor());
return (argb & 0x00FFFFFF) | ((na & 0xFF) << 24);
}
}

View file

@ -7,6 +7,8 @@
package dev.loki.lovisual.features.module.modules.misc;
import dev.loki.lovisual.util.time.GameClock;
import net.fabricmc.fabric.api.event.player.AttackEntityCallback;
import net.minecraft.client.Minecraft;
import net.minecraft.core.registries.BuiltInRegistries;
@ -268,7 +270,7 @@ public class DefineTarget extends Module {
}
private void notifyBlock(Entity target) {
long now = System.currentTimeMillis();
long now = GameClock.millis();
if (now - lastBlockToast < 600) return;
lastBlockToast = now;
String name = target.getName() != null ? target.getName().getString() : target.getType().toString();

View file

@ -7,6 +7,8 @@
package dev.loki.lovisual.features.module.modules.player;
import dev.loki.lovisual.util.time.GameClock;
import net.minecraft.client.Minecraft;
import net.minecraft.core.BlockPos;
import net.minecraft.world.item.ItemStack;
@ -66,13 +68,13 @@ public class AutoTool extends Module {
}
InventorySwap.INSTANCE.selectHotbar(bestSlot);
}
lastBreakMs = System.currentTimeMillis();
lastBreakMs = GameClock.millis();
return;
}
}
if (originalSlot >= 0) {
long now = System.currentTimeMillis();
long now = GameClock.millis();
if (now - lastBreakMs >= restoreDelayMs.get()) {
InventorySwap.INSTANCE.selectHotbar(originalSlot);
originalSlot = -1;

View file

@ -7,6 +7,8 @@
package dev.loki.lovisual.features.module.modules.player;
import dev.loki.lovisual.util.time.GameClock;
import dev.loki.lovisual.config.common.CommonSettingSchemas;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.LocalPlayer;
@ -184,7 +186,7 @@ public final class NoDelay extends Module {
return false;
}
long now = System.currentTimeMillis();
long now = GameClock.millis();
if (ctx.target == UseTarget.THROWABLE) {
if (pressStartThrow == 0L) {
pressStartThrow = now;

View file

@ -28,8 +28,6 @@ import java.util.List;
)
public class ShitDropper extends Module {
private static final Minecraft mc = Minecraft.getInstance();
private static final String SETTING_TRASH_ITEM_IDS = "trash_item_ids";
private static final String SETTING_MAX_TRASH_SLOTS = "max_trash_slots";
private static final String SETTING_STACKS_PER_TICK = "stacks_per_tick";
@ -79,6 +77,7 @@ public class ShitDropper extends Module {
@Override
public void onTick() {
Minecraft mc = Minecraft.getInstance();
if (!isEnabled() || mc.player == null || mc.gameMode == null) return;
var handler = mc.player.containerMenu;

View file

@ -0,0 +1,287 @@
package dev.loki.lovisual.features.module.modules.visuals;
import dev.loki.lovisual.config.values.*;
import dev.loki.lovisual.features.module.Module;
import dev.loki.lovisual.features.module.ModuleCategory;
import dev.loki.lovisual.features.module.ModuleInfo;
import dev.loki.lovisual.features.module.WorldPhase;
import dev.loki.lovisual.features.theme.Theme;
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
import dev.loki.lovisual.util.time.GameClock;
import net.minecraft.client.Minecraft;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.phys.Vec3;
//todo Description
@ModuleInfo(id = "backsword", displayName = "BackSword", category = ModuleCategory.VISUALS)
public class BackSword extends Module {
// базовый подъём катаны, чтобы лежала на спине, а не у ног
private static final float BACK_SWORD_LIFT = 0.85f;
private static final String SETTING_TARGET = "target";
private static final String SETTING_COLOR_MODE = "color_mode";
private static final String SETTING_CUSTOM_COLOR = "custom_color";
private static final String SETTING_GLOW = "glow";
private static final String SETTING_GLOW_LEVEL = "glow_level";
private static final String SETTING_FILL_ALPHA = "fill_alpha";
private static final String SETTING_OUTLINE_ALPHA = "outline_alpha";
private static final String SETTING_OFFSET_Y = "offset_y";
private static final String SETTING_OFFSET_Z = "offset_z";
private final ModeValue target = modeSetting(
"backSwordTarget", SETTING_TARGET, "Self", "Self", "All Players", "Self and Others");
private final ModeValue colorMode = modeSetting(
"backSwordColorMode", SETTING_COLOR_MODE, "Theme", "Theme", "Custom", "Rainbow");
private final RGBColorValue customColor =
visibleWhen(colorNoAlpha("backSwordCustomColor", SETTING_CUSTOM_COLOR, "#E91E63"), this::isCustomColor);
private final BooleanValue glow =
bool("backSwordGlow", SETTING_GLOW, true);
private final NumberValue<Float> glowLevel =
visibleWhen(num("backSwordGlowLevel", SETTING_GLOW_LEVEL, 50.0f, 0.0f, 100.0f), glow::get);
private final NumberValue<Float> fillAlpha =
num("backSwordFillAlpha", SETTING_FILL_ALPHA, 20.0f, 0.0f, 100.0f);
private final NumberValue<Float> outlineAlpha =
num("backSwordOutlineAlpha", SETTING_OUTLINE_ALPHA, 85.0f, 0.0f, 100.0f);
private final NumberValue<Float> offsetY =
num("backSwordOffsetY", SETTING_OFFSET_Y, 0.1f, -0.5f, 0.7f);
private final NumberValue<Float> offsetZ =
num("backSwordOffsetZ", SETTING_OFFSET_Z, 0.28f, 0.0f, 0.6f);
@Override
public WorldPhase getWorldPhase() {
return WorldPhase.END_MAIN;
}
@Override
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
Minecraft mc = Minecraft.getInstance();
if (!isEnabled() || mc.level == null || mc.player == null || renderer == null) return;
boolean firstPerson = mc.options.getCameraType().isFirstPerson();
boolean targetSelf = "Self".equals(target.get()) || "Self and Others".equals(target.get());
boolean targetOthers = "All Players".equals(target.get()) || "Self and Others".equals(target.get());
if (!firstPerson && targetSelf && mc.player.isAlive()) {
renderSword(renderer, mc.player, tickDelta);
}
if (targetOthers) {
for (Player player : mc.level.players()) {
if (player == mc.player || !player.isAlive()) continue;
renderSword(renderer, player, tickDelta);
}
}
}
private void renderSword(Renderer3D renderer, Player player, float tickDelta) {
Vec3 pos = player.getPosition(tickDelta);
float bodyYaw = Mth.rotLerp(tickDelta, player.yBodyRotO, player.yBodyRot);
float offY = offsetY.get();
float offZ = offsetZ.get();
MeshBuilder fill = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_DEPTH, Renderer3D.DepthMode.MAIN);
MeshBuilder glowMesh = null;
if (glow.get()) {
glowMesh = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE_DEPTH, Renderer3D.DepthMode.PRE_DEPTH);
}
if (fill == null) return;
int fillA = Math.round(fillAlpha.get() / 100f * 255f);
int outA = Math.round(outlineAlpha.get() / 100f * 255f);
Shade fillShade = shade(fillA);
Shade edgeShade = edgeShade(outA);
drawKatana(fill, pos, bodyYaw, offY, offZ, fillShade);
drawKatanaEdge(fill, pos, bodyYaw, offY, offZ, edgeShade, 0.004f);
if (glowMesh != null) {
float glowBase = glowLevel.get() / 100f;
float pulse = (float) (Math.sin(GameClock.millis() / 700.0) * 0.12 + 0.88);
glowBase *= pulse;
int layers = 12;
float maxEx = 0.045f;
for (int i = 0; i < layers; i++) {
float t = (float) i / (layers - 1);
float falloff = (1f - t) * (1f - t);
int ga = (int) (glowBase * falloff * 180f);
if (ga <= 1) continue;
drawKatanaGlow(glowMesh, pos, bodyYaw, offY, offZ, shade(ga), t * maxEx);
}
}
}
private Shade shade(int alpha) {
return switch (colorMode.get()) {
case "Custom" -> {
int c = customColor.getArgb();
yield fixedShade(c, alpha);
}
case "Rainbow" -> (lx, ly, lz) -> {
float hue = ((ly + 0.6f) * 0.8f + (GameClock.millis() / 1000f) * 0.15f) % 1.0f;
int[] rgb = hsvToRgb(hue, 0.85f, 1.0f);
return argb(rgb[0], rgb[1], rgb[2], alpha);
};
default -> {
yield fixedShade(Theme.theme().accent(), alpha);
}
};
}
private Shade edgeShade(int alpha) {
if ("Rainbow".equals(colorMode.get())) return shade(alpha);
int c = "Custom".equals(colorMode.get()) ? customColor.getArgb() : Theme.theme().accent();
int r = Math.min(((c >>> 16) & 0xFF) + 40, 255);
int g = Math.min(((c >>> 8) & 0xFF) + 40, 255);
int b = Math.min((c & 0xFF) + 40, 255);
return fixedShade(r, g, b, alpha);
}
private static Shade fixedShade(int argb, int alpha) {
int r = (argb >>> 16) & 0xFF, g = (argb >>> 8) & 0xFF, b = argb & 0xFF;
return (lx, ly, lz) -> argb(r, g, b, alpha);
}
private static Shade fixedShade(int r, int g, int b, int alpha) {
return (lx, ly, lz) -> argb(r, g, b, alpha);
}
private boolean isCustomColor() {
return "Custom".equals(colorMode.get());
}
// ===================== Геометрия катаны =====================
private static void drawKatana(MeshBuilder mesh, Vec3 pos, float bodyYaw, float offY, float offZ, Shade shade) {
drawBox(mesh, pos, bodyYaw, offY, offZ, shade, -0.028f, -0.42f, -0.016f, 0.028f, 0.0f, 0.016f);
drawBox(mesh, pos, bodyYaw, offY, offZ, shade, -0.032f, -0.46f, -0.018f, 0.032f, -0.42f, 0.018f);
drawBox(mesh, pos, bodyYaw, offY, offZ, shade, -0.085f, -0.005f, -0.034f, 0.085f, 0.005f, 0.034f);
drawBlade(mesh, pos, bodyYaw, offY, offZ, shade);
}
private static void drawBlade(MeshBuilder mesh, Vec3 pos, float bodyYaw, float offY, float offZ, Shade shade) {
float bw = 0.022f, bBack = 0.006f, bEdge = 0.004f;
float bx0 = -bw, bx1 = bw, by = 0.005f;
float bzBack = bBack, bzEdge = -bEdge;
float mw = 0.015f, mBack = 0.004f, mEdge = 0.002f;
float mx0 = -mw, mx1 = mw, my = 0.50f;
float mzBack = mBack, mzEdge = -mEdge;
float tx = 0f, ty = 1.02f, tz = 0f;
quad(mesh, pos, bodyYaw, offY, offZ, shade, bx0, by, bzBack, bx1, by, bzBack, mx1, my, mzBack, mx0, my, mzBack);
quad(mesh, pos, bodyYaw, offY, offZ, shade, bx1, by, bzBack, bx1, by, bzEdge, mx1, my, mzEdge, mx1, my, mzBack);
quad(mesh, pos, bodyYaw, offY, offZ, shade, bx0, by, bzBack, bx0, by, bzEdge, mx0, my, mzEdge, mx0, my, mzBack);
quad(mesh, pos, bodyYaw, offY, offZ, shade, bx0, by, bzEdge, bx1, by, bzEdge, mx1, my, mzEdge, mx0, my, mzEdge);
quad(mesh, pos, bodyYaw, offY, offZ, shade, mx0, my, mzBack, mx1, my, mzBack, tx, ty, tz, tx, ty, tz);
quad(mesh, pos, bodyYaw, offY, offZ, shade, mx1, my, mzBack, mx1, my, mzEdge, tx, ty, tz, tx, ty, tz);
quad(mesh, pos, bodyYaw, offY, offZ, shade, mx0, my, mzBack, mx0, my, mzEdge, tx, ty, tz, tx, ty, tz);
quad(mesh, pos, bodyYaw, offY, offZ, shade, mx0, my, mzEdge, mx1, my, mzEdge, tx, ty, tz, tx, ty, tz);
}
private static void drawKatanaEdge(MeshBuilder mesh, Vec3 pos, float bodyYaw, float offY, float offZ, Shade shade, float e) {
drawBox(mesh, pos, bodyYaw, offY, offZ, shade, -0.028f - e, -0.42f - e, -0.016f - e, 0.028f + e, 0.0f + e, 0.016f + e);
drawBox(mesh, pos, bodyYaw, offY, offZ, shade, -0.032f - e, -0.46f - e, -0.018f - e, 0.032f + e, -0.42f + e, 0.018f + e);
drawBox(mesh, pos, bodyYaw, offY, offZ, shade, -0.085f - e, -0.005f - e, -0.034f - e, 0.085f + e, 0.005f + e, 0.034f + e);
drawBlade(mesh, pos, bodyYaw, offY, offZ, shade);
}
private static void drawKatanaGlow(MeshBuilder mesh, Vec3 pos, float bodyYaw, float offY, float offZ, Shade shade, float ex) {
drawBox(mesh, pos, bodyYaw, offY, offZ, shade, -0.028f - ex, -0.42f - ex, -0.016f - ex, 0.028f + ex, 0.0f + ex, 0.016f + ex);
drawBox(mesh, pos, bodyYaw, offY, offZ, shade, -0.032f - ex, -0.46f - ex, -0.018f - ex, 0.032f + ex, -0.42f + ex, 0.018f + ex);
drawBox(mesh, pos, bodyYaw, offY, offZ, shade, -0.085f - ex, -0.005f - ex, -0.034f - ex, 0.085f + ex, 0.005f + ex, 0.034f + ex);
float bw = 0.022f + ex, bBack = 0.006f + ex, bEdge = 0.004f + ex;
float mw = 0.015f + ex, mBack = 0.004f + ex, mEdge = 0.002f + ex;
float bx0 = -bw, bx1 = bw, by = 0.005f - ex;
float mx0 = -mw, mx1 = mw, my = 0.50f;
float mzBack = mBack, mzEdge = -mEdge;
float bzBack = bBack, bzEdge = -bEdge;
float tx = 0f, ty = 1.02f + ex, tz = 0f;
quad(mesh, pos, bodyYaw, offY, offZ, shade, bx0, by, bzBack, bx1, by, bzBack, mx1, my, mzBack, mx0, my, mzBack);
quad(mesh, pos, bodyYaw, offY, offZ, shade, bx1, by, bzBack, bx1, by, bzEdge, mx1, my, mzEdge, mx1, my, mzBack);
quad(mesh, pos, bodyYaw, offY, offZ, shade, bx0, by, bzBack, bx0, by, bzEdge, mx0, my, mzEdge, mx0, my, mzBack);
quad(mesh, pos, bodyYaw, offY, offZ, shade, bx0, by, bzEdge, bx1, by, bzEdge, mx1, my, mzEdge, mx0, my, mzEdge);
quad(mesh, pos, bodyYaw, offY, offZ, shade, mx0, my, mzBack, mx1, my, mzBack, tx, ty, tz, tx, ty, tz);
quad(mesh, pos, bodyYaw, offY, offZ, shade, mx1, my, mzBack, mx1, my, mzEdge, tx, ty, tz, tx, ty, tz);
quad(mesh, pos, bodyYaw, offY, offZ, shade, mx0, my, mzBack, mx0, my, mzEdge, tx, ty, tz, tx, ty, tz);
quad(mesh, pos, bodyYaw, offY, offZ, shade, mx0, my, mzEdge, mx1, my, mzEdge, tx, ty, tz, tx, ty, tz);
}
private static void drawBox(MeshBuilder mesh, Vec3 pos, float bodyYaw, float offY, float offZ, Shade shade,
float x0, float y0, float z0, float x1, float y1, float z1) {
quad(mesh, pos, bodyYaw, offY, offZ, shade, x0, y0, z0, x1, y0, z0, x1, y0, z1, x0, y0, z1);
quad(mesh, pos, bodyYaw, offY, offZ, shade, x0, y1, z0, x0, y1, z1, x1, y1, z1, x1, y1, z0);
quad(mesh, pos, bodyYaw, offY, offZ, shade, x0, y0, z0, x0, y1, z0, x1, y1, z0, x1, y0, z0);
quad(mesh, pos, bodyYaw, offY, offZ, shade, x0, y0, z1, x1, y0, z1, x1, y1, z1, x0, y1, z1);
quad(mesh, pos, bodyYaw, offY, offZ, shade, x0, y0, z0, x0, y0, z1, x0, y1, z1, x0, y1, z0);
quad(mesh, pos, bodyYaw, offY, offZ, shade, x1, y0, z0, x1, y1, z0, x1, y1, z1, x1, y0, z1);
}
private static void quad(MeshBuilder mesh, Vec3 pos, float bodyYaw, float offY, float offZ, Shade shade,
float x0, float y0, float z0, float x1, float y1, float z1,
float x2, float y2, float z2, float x3, float y3, float z3) {
mesh.ensureQuadCapacity();
int i0 = addVertex(mesh, pos, bodyYaw, offY, offZ, x0, y0, z0, shade.at(x0, y0, z0));
int i1 = addVertex(mesh, pos, bodyYaw, offY, offZ, x1, y1, z1, shade.at(x1, y1, z1));
int i2 = addVertex(mesh, pos, bodyYaw, offY, offZ, x2, y2, z2, shade.at(x2, y2, z2));
int i3 = addVertex(mesh, pos, bodyYaw, offY, offZ, x3, y3, z3, shade.at(x3, y3, z3));
mesh.quad(i0, i1, i2, i3);
}
private static int addVertex(MeshBuilder mesh, Vec3 pos, float bodyYaw, float offY, float offZ,
float lx, float ly, float lz, int argb) {
Vec3 w = katanaVertex(lx, ly, lz, pos, bodyYaw, offY, offZ);
mesh.vec3(w.x, w.y, w.z)
.color((argb >>> 16) & 0xFF, (argb >>> 8) & 0xFF, argb & 0xFF, (argb >>> 24) & 0xFF);
return mesh.next();
}
private static Vec3 katanaVertex(float lx, float ly, float lz, Vec3 pos, float bodyYaw, float offY, float offZ) {
float x = lx * 1.15f, y = ly * 1.15f, z = lz * 1.15f;
float c45 = 0.7071067812f, s45 = -0.7071067812f;
float rzx = x * c45 - y * s45;
float rzy = x * s45 + y * c45;
float c3 = 0.9986295347f, s3 = -0.0523359562f;
float rxy = rzy * c3 - z * s3;
float rxz = rzy * s3 + z * c3;
float ox = rzx - 0.15f, oy = rxy + offY + BACK_SWORD_LIFT, oz = rxz + offZ;
float yaw = (float) Math.toRadians(180f - bodyYaw);
float cy = (float) Math.cos(yaw), sy = (float) Math.sin(yaw);
float wx = ox * cy + oz * sy;
float wz = -ox * sy + oz * cy;
return new Vec3(pos.x + wx, pos.y + oy, pos.z + wz);
}
private static int argb(int r, int g, int b, int a) {
return (Mth.clamp(a, 0, 255) << 24) | (Mth.clamp(r, 0, 255) << 16) | (Mth.clamp(g, 0, 255) << 8) | Mth.clamp(b, 0, 255);
}
private static int[] hsvToRgb(float h, float s, float v) {
int i = (int) (h * 6f);
float f = h * 6f - i;
float p = v * (1 - s);
float q = v * (1 - f * s);
float t = v * (1 - (1 - f) * s);
float r, g, b;
switch (i % 6) {
case 0 -> { r = v; g = t; b = p; }
case 1 -> { r = q; g = v; b = p; }
case 2 -> { r = p; g = v; b = t; }
case 3 -> { r = p; g = q; b = v; }
case 4 -> { r = t; g = p; b = v; }
default -> { r = v; g = p; b = q; }
}
return new int[]{(int) (r * 255), (int) (g * 255), (int) (b * 255)};
}
private interface Shade {
int at(float lx, float ly, float lz);
}
}

View file

@ -10,6 +10,7 @@ package dev.loki.lovisual.features.module.modules.visuals;
import dev.loki.lovisual.features.theme.Theme;
import com.mojang.blaze3d.GpuFormat;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import com.mojang.blaze3d.pipeline.TextureTarget;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.textures.AddressMode;
@ -39,6 +40,8 @@ import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
import dev.loki.lovisual.render.engine.postprocess.PostProcessManager;
import dev.loki.lovisual.render.engine.postprocess.PostProcessPass;
import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer;
import dev.loki.lovisual.render.engine.uniform.impl.HandBlendUniforms;
import dev.loki.lovisual.render.engine.uniform.impl.HandChamsUniforms;
import dev.loki.lovisual.render.engine.uniform.impl.HandGlassUniforms;
import dev.loki.lovisual.render.engine.uniform.impl.HandMetallicUniforms;
import dev.loki.lovisual.render.engine.uniform.impl.HandSmokeUniforms;
@ -81,9 +84,40 @@ public class Chams extends Module {
private static final String SETTING_METALLIC_BRUSHED_LINES = "metallic_brushed_lines";
private static final String SETTING_METALLIC_FLAKES = "metallic_flakes";
private static final String SETTING_METALLIC_PRISM = "metallic_prism";
private static final String SETTING_DUAL_SHADER = "dual_shader";
private static final String SETTING_SHADER_1 = "shader1";
private static final String SETTING_SHADER_2 = "shader2";
private static final String SETTING_BLEND_MODE = "blend_mode";
private static final String SETTING_BLEND_INTENSITY = "blend_intensity";
private static final String SETTING_SHADER_1_STRENGTH = "shader1_strength";
private static final String SETTING_SHADER_2_STRENGTH = "shader2_strength";
private static final String SETTING_SNOW_COLOR = "snow_color";
private static final String SETTING_SNOW_INTENSITY = "snow_intensity";
private static final String SETTING_SNOW_SPEED = "snow_speed";
private static final String SETTING_CHROMA_SPEED = "chroma_speed";
private static final String SETTING_CHROMA_SATURATION = "chroma_saturation";
private static final String SETTING_CHROMA_BRIGHTNESS = "chroma_brightness";
private static final String SETTING_SOLID_COLOR1 = "solid_color1";
private static final String SETTING_SOLID_COLOR2 = "solid_color2";
private static final String SETTING_SOLID_SPEED = "solid_speed";
private static final String SETTING_STRIPES_COLOR1 = "stripes_color1";
private static final String SETTING_STRIPES_COLOR2 = "stripes_color2";
private static final String SETTING_STRIPES_WIDTH = "stripes_width";
private static final String SETTING_STRIPES_SPEED = "stripes_speed";
private static final String SETTING_GLOW_SHADER_FILL = "glow_shader_fill";
private static final String SETTING_GLOW_SHADER_RADIUS = "glow_shader_radius";
private static final String SETTING_GLOW_SHADER_POWER = "glow_shader_power";
private static final String SETTING_GLOW_SHADER_DISPERSION = "glow_shader_dispersion";
private static final String SETTING_GLOW_SHADER_COLOR = "glow_shader_color";
private static final String SETTING_BALATRO_COLOR1 = "balatro_color1";
private static final String SETTING_BALATRO_COLOR2 = "balatro_color2";
private static final String SETTING_BALATRO_COLOR3 = "balatro_color3";
private static final String SETTING_BALATRO_CONTRAST = "balatro_contrast";
private static final String SETTING_BALATRO_LIGHTING = "balatro_lighting";
private static final String SETTING_HIDE_HAND = "hide_hand";
private final Minecraft mc = Minecraft.getInstance();
private final ModeValue mode =
modeSetting("handChamsMode", SETTING_MODE, "Smoke", "Smoke", "Metallic", "Glass");
modeSetting("handChamsMode", SETTING_MODE, "Smoke", "Smoke", "Metallic", "Glass", "Snow", "Chroma", "Solid", "Stripes", "Glow", "Balatro");
private final BooleanValue hands =
bool("chamsHands", SETTING_HANDS, true);
@ -147,8 +181,72 @@ public class Chams extends Module {
visibleWhen(num("handChamsMetallicFlakes", SETTING_METALLIC_FLAKES, 0.25f, 0.0f, 1.0f), this::isMetallicMode);
private final NumberValue<Float> metallicPrism =
visibleWhen(num("handChamsMetallicPrism", SETTING_METALLIC_PRISM, 0.18f, 0.0f, 1.0f), this::isMetallicMode);
private final BooleanValue dualShader =
bool("chamsDualShader", SETTING_DUAL_SHADER, false);
private final ModeValue shader1 =
visibleWhen(modeSetting("chamsShader1", SETTING_SHADER_1, "Glow", "Snow", "Chroma", "Solid", "Stripes", "Glow", "Balatro"), dualShader::get);
private final ModeValue shader2 =
visibleWhen(modeSetting("chamsShader2", SETTING_SHADER_2, "Chroma", "Snow", "Chroma", "Solid", "Stripes", "Glow", "Balatro"), dualShader::get);
private final ModeValue blendMode =
visibleWhen(modeSetting("chamsBlendMode", SETTING_BLEND_MODE, "Add", "Mix", "Add", "Multiply", "Screen", "Overlay", "Difference"), dualShader::get);
private final NumberValue<Float> blendIntensity =
visibleWhen(num("chamsBlendIntensity", SETTING_BLEND_INTENSITY, 1.0f, 0.0f, 1.0f), dualShader::get);
private final NumberValue<Float> shader1Strength =
visibleWhen(num("chamsShader1Strength", SETTING_SHADER_1_STRENGTH, 1.0f, 0.0f, 1.0f), dualShader::get);
private final NumberValue<Float> shader2Strength =
visibleWhen(num("chamsShader2Strength", SETTING_SHADER_2_STRENGTH, 1.0f, 0.0f, 1.0f), dualShader::get);
private final RGBColorValue snowColor =
visibleWhen(colorNoAlpha("chamsSnowColor", SETTING_SNOW_COLOR, "#FFFFFF"), this::isSnowMode);
private final NumberValue<Float> snowIntensity =
visibleWhen(num("chamsSnowIntensity", SETTING_SNOW_INTENSITY, 1.0f, 0.1f, 3.0f), this::isSnowMode);
private final NumberValue<Float> snowSpeed =
visibleWhen(num("chamsSnowSpeed", SETTING_SNOW_SPEED, 1.0f, 0.1f, 2.0f), this::isSnowMode);
private final NumberValue<Float> chromaSpeed =
visibleWhen(num("chamsChromaSpeed", SETTING_CHROMA_SPEED, 0.3f, 0.05f, 1.0f), this::isChromaMode);
private final NumberValue<Float> chromaSaturation =
visibleWhen(num("chamsChromaSaturation", SETTING_CHROMA_SATURATION, 0.5f, 0.1f, 1.0f), this::isChromaMode);
private final NumberValue<Float> chromaBrightness =
visibleWhen(num("chamsChromaBrightness", SETTING_CHROMA_BRIGHTNESS, 1.0f, 0.5f, 1.5f), this::isChromaMode);
private final RGBColorValue solidColor1 =
visibleWhen(colorNoAlpha("chamsSolidColor1", SETTING_SOLID_COLOR1, "#E91E63"), this::isSolidMode);
private final RGBColorValue solidColor2 =
visibleWhen(colorNoAlpha("chamsSolidColor2", SETTING_SOLID_COLOR2, "#9B59B6"), this::isSolidMode);
private final NumberValue<Float> solidSpeed =
visibleWhen(num("chamsSolidSpeed", SETTING_SOLID_SPEED, 0.5f, 0.1f, 1.0f), this::isSolidMode);
private final RGBColorValue stripesColor1 =
visibleWhen(colorNoAlpha("chamsStripesColor1", SETTING_STRIPES_COLOR1, "#778889"), this::isStripesMode);
private final RGBColorValue stripesColor2 =
visibleWhen(colorNoAlpha("chamsStripesColor2", SETTING_STRIPES_COLOR2, "#5A6566"), this::isStripesMode);
private final NumberValue<Float> stripesWidth =
visibleWhen(num("chamsStripesWidth", SETTING_STRIPES_WIDTH, 0.3f, 0.05f, 0.5f), this::isStripesMode);
private final NumberValue<Float> stripesSpeed =
visibleWhen(num("chamsStripesSpeed", SETTING_STRIPES_SPEED, 0.3f, 0.1f, 2.0f), this::isStripesMode);
private final BooleanValue glowShaderFill =
visibleWhen(bool("chamsGlowShaderFill", SETTING_GLOW_SHADER_FILL, true), this::isGlowShaderMode);
private final NumberValue<Float> glowShaderRadius =
visibleWhen(num("chamsGlowShaderRadius", SETTING_GLOW_SHADER_RADIUS, 0.04f, 0.01f, 0.2f), this::isGlowShaderMode);
private final NumberValue<Float> glowShaderPower =
visibleWhen(num("chamsGlowShaderPower", SETTING_GLOW_SHADER_POWER, 0.75f, 0.1f, 2.0f), this::isGlowShaderMode);
private final NumberValue<Float> glowShaderDispersion =
visibleWhen(num("chamsGlowShaderDispersion", SETTING_GLOW_SHADER_DISPERSION, 5.0f, 1.0f, 10.0f), this::isGlowShaderMode);
private final RGBColorValue glowShaderColor =
visibleWhen(colorNoAlpha("chamsGlowShaderColor", SETTING_GLOW_SHADER_COLOR, "#3498DB"), this::isGlowShaderMode);
private final RGBColorValue balatroColor1 =
visibleWhen(colorNoAlpha("chamsBalatroColor1", SETTING_BALATRO_COLOR1, "#DE443B"), this::isBalatroMode);
private final RGBColorValue balatroColor2 =
visibleWhen(colorNoAlpha("chamsBalatroColor2", SETTING_BALATRO_COLOR2, "#006BB4"), this::isBalatroMode);
private final RGBColorValue balatroColor3 =
visibleWhen(colorNoAlpha("chamsBalatroColor3", SETTING_BALATRO_COLOR3, "#162325"), this::isBalatroMode);
private final NumberValue<Float> balatroContrast =
visibleWhen(num("chamsBalatroContrast", SETTING_BALATRO_CONTRAST, 0.6f, 0.1f, 1.0f), this::isBalatroMode);
private final NumberValue<Float> balatroLighting =
visibleWhen(num("chamsBalatroLighting", SETTING_BALATRO_LIGHTING, 0.6f, 0.0f, 1.0f), this::isBalatroMode);
private final BooleanValue hideHand =
bool("chamsHideHand", SETTING_HIDE_HAND, false);
private final PostProcessPass handsPass = new HandsPass();
private TextureTarget handMask;
private TextureTarget dualA;
private TextureTarget dualB;
private GpuSampler handMaskSampler;
private int bufferW = -1;
private int bufferH = -1;
@ -475,6 +573,43 @@ public class Chams extends Module {
return "Glass".equals(mode.get());
}
private boolean isSnowMode() {
return "Snow".equals(mode.get());
}
private boolean isChromaMode() {
return "Chroma".equals(mode.get());
}
private boolean isSolidMode() {
return "Solid".equals(mode.get());
}
private boolean isStripesMode() {
return "Stripes".equals(mode.get());
}
private boolean isGlowShaderMode() {
return "Glow".equals(mode.get());
}
private boolean isBalatroMode() {
return "Balatro".equals(mode.get());
}
private void ensureDualBuffers() {
int w = mc.getWindow().getWidth();
int h = mc.getWindow().getHeight();
if (w <= 0 || h <= 0) return;
if (dualA == null) {
dualA = new TextureTarget("lovisual-hand-dual-a", w, h, true, GpuFormat.RGBA8_UNORM);
dualB = new TextureTarget("lovisual-hand-dual-b", w, h, true, GpuFormat.RGBA8_UNORM);
} else if (w != bufferW || h != bufferH) {
dualA.resize(w, h);
dualB.resize(w, h);
}
}
private int materialFillRgb() {
int fill = fillColor.getArgb();
if ("Theme".equals(visualColorSource.get())) {
@ -530,6 +665,12 @@ public class Chams extends Module {
float rawTime = getRawTime();
if (dualShader.get()) {
renderDualShader(src, dst, maskView, rawTime);
maskReady = false;
return true;
}
if ("Glass".equals(mode.get())) {
int fillArgb = fillColor.getArgb();
@ -540,7 +681,7 @@ public class Chams extends Module {
int w = mc.getWindow().getWidth();
int h = mc.getWindow().getHeight();
float glassAlpha = clamp01(fillA);
float glassAlpha = hideHand.get() ? 1.0f : clamp01(fillA);
float glassStrength = Math.max(0.0f, fillA * 4.0f);
float glassLevel = Math.min(glassStrength, 4.0f);
@ -576,7 +717,7 @@ public class Chams extends Module {
int glowRgb = glowRgb(baseRgb);
int shadowRgb = shadowRgb(baseRgb);
float fillA = channel(fillArgb, 24);
float fillA = hideHand.get() ? 1.0f : channel(fillArgb, 24);
float glowA = glow.get() ? glowAlpha.get() : 0.0f;
float shadowA = shadow.get() ? shadowAlpha.get() : 0.0f;
@ -597,13 +738,13 @@ public class Chams extends Module {
.sampler("u_Src", src, PostProcessManager.getSampler())
.sampler("u_Mask", maskView, getHandMaskSampler())
.end();
} else {
} else if ("Smoke".equals(mode.get())) {
int fillArgb = fillColor.getArgb();
int fillRgb = materialFillRgb();
int glowRgb = glowRgb(fillRgb);
int shadowRgb = shadowRgb(fillRgb);
float fillA = channel(fillArgb, 24);
float fillA = hideHand.get() ? 1.0f : channel(fillArgb, 24);
float glowA = glow.get() ? glowAlpha.get() : 0.0f;
float shadowA = shadow.get() ? shadowAlpha.get() : 0.0f;
@ -626,12 +767,130 @@ public class Chams extends Module {
.sampler("u_Src", src, PostProcessManager.getSampler())
.sampler("u_Mask", maskView, getHandMaskSampler())
.end();
} else {
renderChamsEffect(src, dst, maskView, mode.get(), rawTime);
}
maskReady = false;
return true;
}
private void renderChamsEffect(GpuTextureView src, GpuTextureView dst, GpuTextureView maskView, String chamsMode, float rawTime) {
int w = mc.getWindow().getWidth();
int h = mc.getWindow().getHeight();
float time = rawTime;
int colorA = 0xFFFFFFFF, colorB = 0xFFFFFFFF, colorC = 0xFFFFFFFF, colorD = 0xFFFFFFFF;
float chromaSpeedV = 0.3f, chromaSat = 0.5f, chromaBright = 1.0f;
float snowInt = 1.0f, snowSpd = 1.0f;
float stripeW = 0.3f, stripeSpd = 0.3f;
boolean glowFill = true;
float glowRadiusV = 0.04f, glowPowerV = 0.75f, glowDisp = 5.0f;
switch (chamsMode) {
case "Snow" -> {
colorA = snowColor.getArgb();
snowInt = snowIntensity.get();
snowSpd = snowSpeed.get();
}
case "Chroma" -> {
chromaSpeedV = chromaSpeed.get();
chromaSat = chromaSaturation.get();
chromaBright = chromaBrightness.get();
}
case "Solid" -> {
colorA = solidColor1.getArgb();
colorB = solidColor2.getArgb();
time = rawTime * solidSpeed.get();
}
case "Stripes" -> {
colorA = stripesColor1.getArgb();
colorB = stripesColor2.getArgb();
stripeW = stripesWidth.get();
stripeSpd = stripesSpeed.get();
}
case "Glow" -> {
colorC = glowShaderColor.getArgb();
glowFill = glowShaderFill.get();
glowRadiusV = glowShaderRadius.get();
glowPowerV = glowShaderPower.get();
glowDisp = glowShaderDispersion.get();
}
default -> {
colorA = balatroColor1.getArgb();
colorB = balatroColor2.getArgb();
colorC = balatroColor3.getArgb();
colorD = rgb01(balatroContrast.get(), balatroLighting.get());
}
}
HandChamsUniforms.update(colorA, colorB, colorC, colorD, w, h, time,
hideHand.get() ? 1.0f : 0.85f, chromaSpeedV, chromaSat, chromaBright, snowInt, snowSpd, stripeW, stripeSpd,
glowFill, glowRadiusV, glowPowerV, glowDisp, hideHand.get());
RenderPipeline pipeline = switch (chamsMode) {
case "Snow" -> LoVisualRenderPipelines.HAND_CHAMS_SNOW;
case "Chroma" -> LoVisualRenderPipelines.HAND_CHAMS_CHROMA;
case "Solid" -> LoVisualRenderPipelines.HAND_CHAMS_SOLID;
case "Stripes" -> LoVisualRenderPipelines.HAND_CHAMS_STRIPES;
case "Glow" -> LoVisualRenderPipelines.HAND_CHAMS_GLOW;
default -> LoVisualRenderPipelines.HAND_CHAMS_BALATRO;
};
FullScreenRenderer.begin("LoVisual Fullscreen Pass")
.attachment(dst)
.pipeline(pipeline)
.uniform("HandChams", HandChamsUniforms.get())
.sampler("u_Src", src, PostProcessManager.getSampler())
.sampler("u_Mask", maskView, getHandMaskSampler())
.end();
}
private void renderDualShader(GpuTextureView src, GpuTextureView dst, GpuTextureView maskView, float rawTime) {
ensureDualBuffers();
if (dualA == null || dualB == null
|| dualA.getColorTextureView() == null || dualB.getColorTextureView() == null) {
return;
}
clearTexture(dualA);
clearTexture(dualB);
renderChamsEffect(src, dualA.getColorTextureView(), maskView, shader1.get(), rawTime);
renderChamsEffect(src, dualB.getColorTextureView(), maskView, shader2.get(), rawTime);
HandBlendUniforms.update(blendModeIndex(), blendIntensity.get(), shader1Strength.get(), shader2Strength.get());
FullScreenRenderer.begin("LoVisual Fullscreen Pass")
.attachment(dst)
.pipeline(LoVisualRenderPipelines.HAND_CHAMS_BLEND)
.uniform("HandBlend", HandBlendUniforms.get())
.sampler("u_Shader1", dualA.getColorTextureView(), PostProcessManager.getSampler())
.sampler("u_Shader2", dualB.getColorTextureView(), PostProcessManager.getSampler())
.sampler("u_Mask", maskView, getHandMaskSampler())
.end();
}
private int blendModeIndex() {
return switch (blendMode.get()) {
case "Add" -> 1;
case "Multiply" -> 2;
case "Screen" -> 3;
case "Overlay" -> 4;
case "Difference" -> 5;
default -> 0;
};
}
private static int rgb01(float r01, float g01) {
int r = Math.round(clamp01(r01) * 255f);
int g = Math.round(clamp01(g01) * 255f);
return 0xFF000000 | (r << 16) | (g << 8);
}
private static void clearTexture(TextureTarget target) {
if (target == null || target.getColorTexture() == null) return;
var encoder = RenderSystem.getDevice().createCommandEncoder();
encoder.clearColorTexture(target.getColorTexture(), new org.joml.Vector4f(0.0f, 0.0f, 0.0f, 0.0f));
}
private float getRawTime() {
return (float) (Util.getMillis() / 1000.0);
}

View file

@ -7,6 +7,8 @@
package dev.loki.lovisual.features.module.modules.visuals;
import dev.loki.lovisual.util.time.GameClock;
import dev.loki.lovisual.features.module.*;
import dev.loki.lovisual.features.module.Module;
import dev.loki.lovisual.render.engine.core.ViewportContext;
@ -370,7 +372,7 @@ public class Crosshair extends Module {
if (event.getPlayer() != mc.player || !(event.getTarget() instanceof Player player)) {
return;
}
lastVulcanReachAttackMs = System.currentTimeMillis();
lastVulcanReachAttackMs = GameClock.millis();
lastVulcanReachTarget = player;
}
@ -385,7 +387,7 @@ public class Crosshair extends Module {
Entity target = mc.level.getEntity(packet.entityId());
if (target instanceof Player player) {
lastVulcanReachAttackMs = System.currentTimeMillis();
lastVulcanReachAttackMs = GameClock.millis();
lastVulcanReachTarget = player;
}
}
@ -599,7 +601,7 @@ public class Crosshair extends Module {
}
private Player resolveVulcanReachTarget() {
long now = System.currentTimeMillis();
long now = GameClock.millis();
HitResult hit = mc.hitResult;
if (hit instanceof EntityHitResult entityHit
&& entityHit.getEntity() instanceof Player player

View file

@ -7,6 +7,8 @@
package dev.loki.lovisual.features.module.modules.visuals;
import dev.loki.lovisual.util.time.GameClock;
import com.mojang.blaze3d.textures.GpuTextureView;
import net.minecraft.client.Minecraft;
import net.minecraft.util.Mth;
@ -111,7 +113,7 @@ public class DamageTint extends Module implements PostProcessPass {
float contrast = t * contrastStrength.get();
if (pulse.get()) {
float time = (System.currentTimeMillis() % 100000L) / 1000.0f;
float time = (GameClock.millis() % 100000L) / 1000.0f;
float wave = Mth.sin(time * pulseSpeed.get() * 6.2831855f);
float amp = Mth.lerp(t, pulseMin.get(), pulseMax.get());
strength *= 1.0f + (wave * amp);
@ -140,7 +142,7 @@ public class DamageTint extends Module implements PostProcessPass {
}
private float smoothTowards(float target) {
long now = System.currentTimeMillis();
long now = GameClock.millis();
float dt = (lastUpdateMs == 0L) ? 0.016f : (now - lastUpdateMs) / 1000.0f;
lastUpdateMs = now;

View file

@ -7,6 +7,8 @@
package dev.loki.lovisual.features.module.modules.visuals;
import dev.loki.lovisual.util.time.GameClock;
import dev.loki.lovisual.render.engine.animation.AnimationUtility;
import net.minecraft.world.phys.AABB;
@ -49,7 +51,7 @@ final class EspColorUtil {
}
static float shaderSmokeTime() {
return (System.currentTimeMillis() % 600_000L) / 1000.0f;
return (GameClock.millis() % 600_000L) / 1000.0f;
}
static int deriveSmokeColor(int rgb, int layer) {

View file

@ -7,6 +7,8 @@
package dev.loki.lovisual.features.module.modules.visuals;
import dev.loki.lovisual.util.time.GameClock;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import dev.loki.lovisual.config.values.*;
import net.minecraft.client.Minecraft;
@ -404,7 +406,7 @@ public class HitEffect extends Module {
private void addWave(BlockPos pos) {
if (mc.level != null) {
waves.add(new WaveEffect(pos, System.currentTimeMillis()));
waves.add(new WaveEffect(pos, GameClock.millis()));
}
}
@ -684,7 +686,7 @@ public class HitEffect extends Module {
}
private boolean isExpired() {
return System.currentTimeMillis() - startTime > Math.max(1L, waveDurationMs.get());
return GameClock.millis() - startTime > Math.max(1L, waveDurationMs.get());
}
private void render(Renderer3D renderer) {
@ -693,7 +695,7 @@ public class HitEffect extends Module {
long durationMs = Math.max(1L, waveDurationMs.get());
int maxRadius = Math.max(1, waveRadius.get());
long elapsed = System.currentTimeMillis() - startTime;
long elapsed = GameClock.millis() - startTime;
float progress = Mth.clamp((float) elapsed / durationMs, 0f, 1f);
float currentRadius = progress * maxRadius;

View file

@ -7,6 +7,8 @@
package dev.loki.lovisual.features.module.modules.visuals;
import dev.loki.lovisual.util.time.GameClock;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.core.particles.ParticleTypes;
@ -149,7 +151,7 @@ public class KillEffect extends Module {
public void onTick() {
if (!isEnabled() || mc.level == null || mc.player == null) return;
long now = System.currentTimeMillis();
long now = GameClock.millis();
handled.entrySet().removeIf(e -> now - e.getValue() > 6000);
for (LivingEntity entity : mc.level.getEntitiesOfClass(LivingEntity.class, mc.player.getBoundingBox().inflate(128), e -> true)) {
@ -461,11 +463,11 @@ public class KillEffect extends Module {
}
private boolean isExpired() {
return System.currentTimeMillis() - createdAtMs > LIGHTNING_LIFE_MS;
return GameClock.millis() - createdAtMs > LIGHTNING_LIFE_MS;
}
private float getAlpha() {
long age = System.currentTimeMillis() - createdAtMs;
long age = GameClock.millis() - createdAtMs;
float appear = smoothProgress(Math.min(1f, age / (float) LIGHTNING_APPEAR_MS));
float fade = 1f;
if (age > LIGHTNING_APPEAR_MS) {

View file

@ -7,6 +7,8 @@
package dev.loki.lovisual.features.module.modules.visuals;
import dev.loki.lovisual.util.time.GameClock;
import com.mojang.blaze3d.textures.GpuTextureView;
import net.minecraft.client.Minecraft;
import dev.loki.lovisual.config.values.ModeValue;
@ -145,7 +147,7 @@ public class PostFX extends Module implements PostProcessPass {
int h = mc.getWindow().getHeight();
float invW = w > 0 ? 1.0f / w : 0.0f;
float invH = h > 0 ? 1.0f / h : 0.0f;
float time = (System.currentTimeMillis() % 100000L) / 1000.0f;
float time = (GameClock.millis() % 100000L) / 1000.0f;
boolean lutAvailable = lutMode > 0.0f;
fillUniforms(invW, invH, time, presetName, isCustom, lutAvailable);

View file

@ -0,0 +1,78 @@
package dev.loki.lovisual.features.module.modules.visuals;
import dev.loki.lovisual.config.values.ModeValue;
import dev.loki.lovisual.config.values.NumberValue;
import dev.loki.lovisual.features.module.Module;
import dev.loki.lovisual.features.module.ModuleCategory;
import dev.loki.lovisual.features.module.ModuleInfo;
//todo Description
@ModuleInfo(id = "shadersky", displayName = "ShaderSky", category = ModuleCategory.VISUALS)
public class ShaderSky extends Module {
private static final String SETTING_MODE = "mode";
private static final String SETTING_SPEED = "speed";
private static final String SETTING_SCALE = "scale";
private static final String SETTING_INTENSITY = "intensity";
private static final String SETTING_ALPHA = "alpha";
private static final String SETTING_THUNDER_INTERVAL = "thunder_interval";
private static final String SETTING_THUNDER_CHANCE = "thunder_chance";
private static final String SETTING_THUNDER_GLOW = "thunder_glow";
private final ModeValue mode = modeSetting(
"shaderSkyMode",
SETTING_MODE,
"Thunder",
"Thunder", "Pulsar"
);
private final NumberValue<Float> speed =
num("shaderSkySpeed", SETTING_SPEED, 1.0f, 0.1f, 5.0f);
private final NumberValue<Float> scale =
num("shaderSkyScale", SETTING_SCALE, 5.0f, 1.0f, 20.0f);
private final NumberValue<Float> intensity =
num("shaderSkyIntensity", SETTING_INTENSITY, 0.01f, 0.001f, 0.05f);
private final NumberValue<Float> alpha =
num("shaderSkyAlpha", SETTING_ALPHA, 1.0f, 0.3f, 1.0f);
private final NumberValue<Float> thunderInterval =
visibleWhen(num("shaderSkyThunderInterval", SETTING_THUNDER_INTERVAL, 4.0f, 1.0f, 10.0f), this::isThunderMode);
private final NumberValue<Float> thunderChance =
visibleWhen(num("shaderSkyThunderChance", SETTING_THUNDER_CHANCE, 0.65f, 0.0f, 1.0f), this::isThunderMode);
private final NumberValue<Float> thunderGlow =
visibleWhen(num("shaderSkyThunderGlow", SETTING_THUNDER_GLOW, 1.0f, 0.1f, 3.0f), this::isThunderMode);
public String getMode() {
return mode.get();
}
public float getSpeed() {
return speed.get();
}
public float getScale() {
return scale.get();
}
public float getIntensity() {
return intensity.get();
}
public float getAlpha() {
return alpha.get();
}
public float getThunderInterval() {
return thunderInterval.get();
}
public float getThunderChance() {
return thunderChance.get();
}
public float getThunderGlow() {
return thunderGlow.get();
}
private boolean isThunderMode() {
return "Thunder".equals(mode.get());
}
}

View file

@ -7,6 +7,8 @@
package dev.loki.lovisual.features.module.modules.visuals;
import dev.loki.lovisual.util.time.GameClock;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.resources.language.I18n;
@ -86,7 +88,7 @@ public class SoundESP extends Module {
if (!isEnabled() || mc.level == null || mc.player == null) return;
if (id == null || pos == null) return;
synchronized (soundLock) {
sounds.add(new SoundPing(id, pos, System.currentTimeMillis()));
sounds.add(new SoundPing(id, pos, GameClock.millis()));
}
}
@ -101,7 +103,7 @@ public class SoundESP extends Module {
public void onRenderHudEngine(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta) {
if (!isEnabled() || mc.level == null || mc.player == null) return;
long now = System.currentTimeMillis();
long now = GameClock.millis();
int lifetime = Math.max(1, lifetimeMs.get());
List<SoundPing> snapshot = new ArrayList<>();
Vec3 camPos = mc.gameRenderer.mainCamera().position();

View file

@ -7,6 +7,9 @@
package dev.loki.lovisual.features.module.modules.visuals;
import dev.loki.lovisual.util.time.GameClock;
import net.minecraft.client.Camera;
import net.minecraft.client.Minecraft;
import net.minecraft.resources.Identifier;
import net.minecraft.util.Mth;
@ -62,6 +65,9 @@ public class TargetESP extends Module {
private static final String SETTING_ESP_AMPLITUDE = "esp_amplitude";
private static final String SETTING_CAPTURE_SPIN_SPEED = "capture_spin_speed";
private static final String SETTING_CRYSTAL_HIT_RED = "crystal_hit_red";
private static final String SETTING_BLACKHOLE_SIZE = "blackhole_size";
private static final String SETTING_BLACKHOLE_SPIN_SPEED = "blackhole_spin_speed";
private static final String SETTING_BLACKHOLE_STYLE = "blackhole_style";
private static final Identifier GHOST_PARTICLE_TEXTURE = TextureStorage.FIRE_FLY;
private static final Identifier CAPTURE_MARK_TEXTURE = TextureStorage.CAPTURE;
private static final Identifier CRYSTAL_BLOOM = TextureStorage.BLOOM;
@ -81,7 +87,7 @@ public class TargetESP extends Module {
"targetEspMode",
SETTING_MODE,
"ghosts",
"ghosts", "ring", "capture_mark", "crystals"
"ghosts", "ring", "capture_mark", "crystals", "blackhole"
);
private final RGBAColorValue colorValue = color("targetEspColor", SETTING_COLOR, "#FFFF5555");
private final ModeValue colorMode = visibleWhen(modeSetting(
@ -118,6 +124,12 @@ public class TargetESP extends Module {
visibleWhen(num("targetEspCaptureSpinSpeed", SETTING_CAPTURE_SPIN_SPEED, 1.0f, 0.2f, 4.0f), this::isCaptureMarkMode);
private final BooleanValue crystalHitRed =
visibleWhen(bool("targetEspCrystalHitRed", SETTING_CRYSTAL_HIT_RED, true), this::isCrystalsMode);
private final NumberValue<Float> blackholeSize =
visibleWhen(num("targetEspBlackholeSize", SETTING_BLACKHOLE_SIZE, 1.1f, 0.5f, 3.0f), this::isBlackholeMode);
private final NumberValue<Float> blackholeSpinSpeed =
visibleWhen(num("targetEspBlackholeSpinSpeed", SETTING_BLACKHOLE_SPIN_SPEED, 1.0f, 0.2f, 4.0f), this::isBlackholeMode);
private final ModeValue blackholeStyle =
visibleWhen(modeSetting("targetEspBlackholeStyle", SETTING_BLACKHOLE_STYLE, "Single", "Single", "Body"), this::isBlackholeMode);
private final List<CrystalInstance> crystalList = new ArrayList<>();
private LivingEntity target;
private LivingEntity renderTarget;
@ -250,6 +262,168 @@ public class TargetESP extends Module {
addRotatedBillboard(mesh, tPosX, cy, tPosZ, size, angle, palette);
}
private static final float[][] BODY_HOLES = {
{0.0f, 0.60f}, // голова
{-0.48f, 0.36f}, // левая рука
{0.48f, 0.36f}, // правая рука
{0.0f, 0.05f}, // грудь
{-0.30f, -0.42f},// левая нога
{0.30f, -0.42f} // правая нога
};
private static void renderBlackhole(Renderer3D renderer,
LivingEntity target,
float tickDelta,
IntFunction<Integer> palette,
float size,
float spinSpeed,
float alpha,
String style) {
if (renderer == null || target == null) return;
Minecraft mc = Minecraft.getInstance();
if (mc.gameRenderer == null || mc.gameRenderer.mainCamera() == null) return;
Vec3 pos = RenderMath.getLerpedPos(target, tickDelta);
Vec3 baseCenter = new Vec3(pos.x, pos.y + target.getBbHeight() * 0.5, pos.z);
// Камера-зависимый billboard-базис: правый/верх в мировых координатах
Camera cam = mc.gameRenderer.mainCamera();
float yaw = (float) Math.toRadians(cam.yRot());
float pitch = (float) Math.toRadians(cam.xRot());
Vec3 fwd = new Vec3(-Math.sin(yaw) * Math.cos(pitch), -Math.sin(pitch), Math.cos(yaw) * Math.cos(pitch)).normalize();
Vec3 rgt = new Vec3(0.0, 1.0, 0.0).cross(fwd);
if (rgt.lengthSqr() < 1.0E-6) {
rgt = new Vec3(1.0, 0.0, 0.0);
} else {
rgt = rgt.normalize();
}
Vec3 upp = fwd.cross(rgt).normalize();
Vector3f right = new Vector3f((float) rgt.x, (float) rgt.y, (float) rgt.z);
Vector3f up = new Vector3f((float) upp.x, (float) upp.y, (float) upp.z);
MeshBuilder diskMesh = renderer.batch(
LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE_DEPTH,
Renderer3D.DepthMode.PRE_DEPTH
);
MeshBuilder coreMesh = renderer.batch(
LoVisualRenderPipelines.WORLD_COLORED_DEPTH,
Renderer3D.DepthMode.PRE_DEPTH
);
if (diskMesh == null || coreMesh == null) return;
float time = (GameClock.millis() % 100000L) / 1000.0f;
if ("Body".equals(style)) {
float holeSize = size * 0.34f;
for (float[] off : BODY_HOLES) {
Vec3 center = new Vec3(
baseCenter.x + rgt.x * off[0] + upp.x * off[1],
baseCenter.y + rgt.y * off[0] + upp.y * off[1],
baseCenter.z + rgt.z * off[0] + upp.z * off[1]
);
appendBlackhole(diskMesh, coreMesh, center, right, up, holeSize, time, spinSpeed, palette, alpha);
}
} else {
appendBlackhole(diskMesh, coreMesh, baseCenter, right, up, size, time, spinSpeed, palette, alpha);
}
}
private static void appendBlackhole(MeshBuilder diskMesh,
MeshBuilder coreMesh,
Vec3 center,
Vector3f right,
Vector3f up,
float size,
float time,
float spinSpeed,
IntFunction<Integer> palette,
float alpha) {
float horizon = 0.35f * size;
int rings = 4;
int segs = 64;
// аккреционный диск: вращающаяся спираль на аддитивном бленде
for (int ring = 0; ring < rings; ring++) {
float r0 = horizon + (size - horizon) * (ring / (float) rings);
float r1 = horizon + (size - horizon) * ((ring + 1) / (float) rings);
for (int s = 0; s < segs; s++) {
float a0 = (float) (Math.PI * 2.0 * s / segs);
float a1 = (float) (Math.PI * 2.0 * (s + 1) / segs);
int v0 = addBlackholeVertex(diskMesh, center, right, up, r0, a0, horizon, size, time, spinSpeed, palette);
int v1 = addBlackholeVertex(diskMesh, center, right, up, r1, a0, horizon, size, time, spinSpeed, palette);
int v2 = addBlackholeVertex(diskMesh, center, right, up, r1, a1, horizon, size, time, spinSpeed, palette);
int v3 = addBlackholeVertex(diskMesh, center, right, up, r0, a1, horizon, size, time, spinSpeed, palette);
diskMesh.triangle(v0, v1, v2);
diskMesh.triangle(v0, v2, v3);
}
}
// чёрное ядро закрывает центр диска
int coreArgb = applyOpacity(0xFF080808, alpha);
int coreCenter = addBlackholeCoreVertex(coreMesh, center, right, up, 0f, 0f, coreArgb);
int coreSegs = 32;
for (int s = 0; s < coreSegs; s++) {
float a0 = (float) (Math.PI * 2.0 * s / coreSegs);
float a1 = (float) (Math.PI * 2.0 * (s + 1) / coreSegs);
int c0 = addBlackholeCoreVertex(coreMesh, center, right, up, horizon, a0, coreArgb);
int c1 = addBlackholeCoreVertex(coreMesh, center, right, up, horizon, a1, coreArgb);
coreMesh.triangle(coreCenter, c1, c0);
}
}
private static int addBlackholeVertex(MeshBuilder mesh,
Vec3 center,
Vector3f right,
Vector3f up,
float r,
float angle,
float horizon,
float size,
float time,
float spinSpeed,
IntFunction<Integer> palette) {
float dist = r / size;
float rot = time * 2.5f * spinSpeed + 1.3f / (dist + 0.05f);
float ca = (float) Math.cos(rot);
float sa = (float) Math.sin(rot);
float ux = dist * (float) Math.cos(angle);
float uy = dist * (float) Math.sin(angle);
float rx = ux * ca - uy * sa;
float ry = ux * sa + uy * ca;
float spiral = Math.abs((float) Math.sin(rx * 14.0f + ry * 4.0f) * 0.5f + 0.5f);
float falloff = (float) Math.pow(horizon / Math.max(dist, 0.001f), 2.2);
float outerFade = smoothstep(1.0f, horizon, dist);
float brightness = falloff * (0.55f + 0.45f * spiral) * outerFade;
int color = palette.apply((int) ((Math.toDegrees(angle) + time * 90f * spinSpeed) % 360f));
color = applyOpacity(color, brightness);
float wx = (float) center.x + right.x() * ux * size + up.x() * uy * size;
float wy = (float) center.y + right.y() * ux * size + up.y() * uy * size;
float wz = (float) center.z + right.z() * ux * size + up.z() * uy * size;
return addVertex(mesh, wx, wy, wz, color);
}
private static int addBlackholeCoreVertex(MeshBuilder mesh,
Vec3 center,
Vector3f right,
Vector3f up,
float r,
float angle,
int argb) {
float ux = r * (float) Math.cos(angle);
float uy = r * (float) Math.sin(angle);
float wx = (float) center.x + right.x() * ux + up.x() * uy;
float wy = (float) center.y + right.y() * ux + up.y() * uy;
float wz = (float) center.z + right.z() * ux + up.z() * uy;
return addVertex(mesh, wx, wy, wz, argb);
}
private static float smoothstep(float edge0, float edge1, float x) {
float t = (x - edge0) / (edge1 - edge0);
t = Math.max(0f, Math.min(1f, t));
return t * t * (3f - 2f * t);
}
private static void appendCrystalGeometry(MeshBuilder mesh,
Vec3 targetPos,
CrystalInstance crystal,
@ -553,6 +727,9 @@ public class TargetESP extends Module {
renderCaptureMark(renderer, renderTarget, tickDelta, palette);
} else if ("crystals".equals(mode)) {
renderCrystals(renderer, renderTarget, tickDelta);
} else if ("blackhole".equals(mode)) {
renderBlackhole(renderer, renderTarget, tickDelta, palette,
blackholeSize.get(), blackholeSpinSpeed.get(), targetAlpha, blackholeStyle.get());
}
}
@ -719,6 +896,10 @@ public class TargetESP extends Module {
return "crystals".equals(espMode.get());
}
private boolean isBlackholeMode() {
return "blackhole".equals(espMode.get());
}
private boolean usesSecondaryColor() {
return AnimatedRenderColors.usesSecondary(animatedColorMode());
}
@ -778,11 +959,11 @@ public class TargetESP extends Module {
}
private float selfRotationDeg() {
return (System.currentTimeMillis() % 36000L) / 100.0f * rotationSpeed;
return (GameClock.millis() % 36000L) / 100.0f * rotationSpeed;
}
private float pulsation() {
return 1.0f + (float) (Math.sin(System.currentTimeMillis() / 500.0) * 0.1f);
return 1.0f + (float) (Math.sin(GameClock.millis() / 500.0) * 0.1f);
}
private void append(MeshBuilder additiveMesh,

View file

@ -7,6 +7,8 @@
package dev.loki.lovisual.features.module.modules.visuals;
import dev.loki.lovisual.util.time.GameClock;
import net.minecraft.client.Minecraft;
import net.minecraft.core.BlockPos;
import net.minecraft.util.Mth;
@ -303,7 +305,7 @@ public final class TazikHat extends Module {
return;
}
long now = System.currentTimeMillis();
long now = GameClock.millis();
for (Player player : mc.level.players()) {
if (player == null || player.isRemoved()) continue;
@ -423,7 +425,7 @@ public final class TazikHat extends Module {
}
private void renderFlying(RenderBuffers buffers, float tickDelta) {
long now = System.currentTimeMillis();
long now = GameClock.millis();
for (FlyingTazik tazik : flying.values()) {
float life = (now - tazik.createdAtMs) / (float) DEATH_LIFE_MS;
@ -654,7 +656,7 @@ public final class TazikHat extends Module {
index
);
float circumference = 0.5f + 0.5f * Mth.sin(index * 0.47f + System.currentTimeMillis() * 0.0007f);
float circumference = 0.5f + 0.5f * Mth.sin(index * 0.47f + GameClock.millis() * 0.0007f);
int mixed = multiplyRgb(dynamic, verticalLight + circumference * 0.16f);
return withAlpha(mixed, Math.round(alpha(mixed) * alphaMul));
@ -669,7 +671,7 @@ public final class TazikHat extends Module {
index + 120
);
float t = 0.5f + 0.5f * Mth.sin(index * 0.71f + System.currentTimeMillis() * 0.0011f);
float t = 0.5f + 0.5f * Mth.sin(index * 0.71f + GameClock.millis() * 0.0011f);
int mixed = mix(dynamic, brighten(palette.primary, 1.35f), t);
return withAlpha(mixed, Math.round(alpha(mixed) * alphaMul));
@ -783,7 +785,7 @@ public final class TazikHat extends Module {
this.scale = scale;
this.palette = palette;
this.seed = seed;
this.createdAtMs = System.currentTimeMillis();
this.createdAtMs = GameClock.millis();
float power = deathFlightPower.get();
@ -797,7 +799,7 @@ public final class TazikHat extends Module {
}
private boolean update() {
long age = System.currentTimeMillis() - createdAtMs;
long age = GameClock.millis() - createdAtMs;
if (age > DEATH_LIFE_MS) {
return true;
}

View file

@ -7,6 +7,8 @@
package dev.loki.lovisual.features.module.modules.visuals;
import dev.loki.lovisual.util.time.GameClock;
import com.mojang.blaze3d.textures.GpuTextureView;
import net.minecraft.client.Minecraft;
import net.minecraft.util.Mth;
@ -70,7 +72,7 @@ public class TotemFX extends Module implements PostProcessPass {
public void onTotemPop() {
if (!isEnabled()) return;
startMs = System.currentTimeMillis();
startMs = GameClock.millis();
}
@Override
@ -107,7 +109,7 @@ public class TotemFX extends Module implements PostProcessPass {
float radius = Mth.clamp(vignetteRadius.get(), 0.05f, 0.95f);
float softness = Mth.clamp(vignetteSoftness.get(), 0.01f, 1.0f);
float time = (System.currentTimeMillis() % 100000L) / 1000.0f;
float time = (GameClock.millis() % 100000L) / 1000.0f;
float pulse = 1.0f + (Mth.sin(time * pulseSpeed.get() * 6.2831855f) * pulseAmount.get());
pulse = Mth.clamp(pulse, 0.0f, 2.0f);
vig *= pulse;
@ -135,7 +137,7 @@ public class TotemFX extends Module implements PostProcessPass {
private float getEnvelope() {
if (startMs < 0L) return 0.0f;
float t = (System.currentTimeMillis() - startMs) / 1000.0f;
float t = (GameClock.millis() - startMs) / 1000.0f;
float in = Math.max(0.01f, fadeIn.get());
float mid = Math.max(0.0f, hold.get());
float out = Math.max(0.01f, fadeOut.get());

View file

@ -7,6 +7,8 @@
package dev.loki.lovisual.features.module.modules.visuals;
import dev.loki.lovisual.util.time.GameClock;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Axis;
import net.minecraft.client.Minecraft;
@ -343,7 +345,7 @@ public class ViewModel extends Module {
case "Helicopter": {
ms.scale(scale, scale, scale);
ms.translate(0.0, -0.2, 0.0);
float rotation = (System.currentTimeMillis() % 2000L) / 2000.0f * 360.0f * power;
float rotation = (GameClock.millis() % 2000L) / 2000.0f * 360.0f * power;
ms.mulPose(Axis.ZP.rotationDegrees(rotation));
ms.translate(0.0, 0.3, 0.0);
break;
@ -438,7 +440,7 @@ public class ViewModel extends Module {
case "Shake": {
ms.scale(scale, scale, scale);
float s = (float) Math.sin((System.currentTimeMillis() % 200L) / 200.0f * Math.PI * 2.0f) * 5.0f;
float s = (float) Math.sin((GameClock.millis() % 200L) / 200.0f * Math.PI * 2.0f) * 5.0f;
ms.mulPose(Axis.ZP.rotationDegrees(s));
break;
}

View file

@ -7,6 +7,8 @@
package dev.loki.lovisual.features.module.modules.visuals;
import dev.loki.lovisual.util.time.GameClock;
import dev.loki.lovisual.config.values.*;
import dev.loki.lovisual.features.theme.Theme;
import net.minecraft.client.Minecraft;
@ -903,7 +905,7 @@ public class WorldParticles extends Module {
this.rotation = Vec3.ZERO;
this.motion = motion;
this.rotationMotion = rotationMotion;
this.spawnMs = System.currentTimeMillis();
this.spawnMs = GameClock.millis();
this.lifeMs = Math.max(1L, lifeMs);
this.size = size;
this.visualPhase = visualPhase;
@ -921,7 +923,7 @@ public class WorldParticles extends Module {
}
private float alpha() {
long age = System.currentTimeMillis() - spawnMs;
long age = GameClock.millis() - spawnMs;
if (age <= 0L) {
return 0.0f;
}
@ -965,7 +967,7 @@ public class WorldParticles extends Module {
}
private long ageMs() {
return System.currentTimeMillis() - spawnMs;
return GameClock.millis() - spawnMs;
}
private float lifeProgress() {
@ -973,7 +975,7 @@ public class WorldParticles extends Module {
}
private boolean isExpired() {
return System.currentTimeMillis() - spawnMs >= lifeMs + ALPHA_ANIM_MS;
return GameClock.millis() - spawnMs >= lifeMs + ALPHA_ANIM_MS;
}
private Vec3 interpolatePos(float tickDelta) {

View file

@ -7,6 +7,8 @@
package dev.loki.lovisual.features.module.modules.visuals;
import dev.loki.lovisual.util.time.GameClock;
import dev.loki.lovisual.config.values.*;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.fog.FogData;
@ -140,7 +142,7 @@ public class WorldTweaks extends Module {
long time = client.level.getGameTime() % 24000L;
return (time + RenderState.tickDelta) / 20.0f;
}
long nowMs = System.currentTimeMillis();
long nowMs = GameClock.millis();
return (nowMs % 100000L) / 1000.0f;
}

View file

@ -19,7 +19,7 @@ public enum CategoryRules {
PlayerRelations rel = PlayerRelations.get();
if (rel.isStaff(name) || StaffHeuristicsConfig.get().matches(name) || matchesTrackedStaff(name))
if (rel.isStaff(name))
return CategoryType.STAFF;
if (rel.isFriend(name))
@ -39,13 +39,4 @@ public enum CategoryRules {
return CategoryType.DEFAULT;
}
private static boolean matchesTrackedStaff(String name) {
if (name == null || name.isBlank()) return false;
for (StaffTracker.StaffInfo info : StaffTracker.all().values()) {
if (info == null || info.name == null || !info.name.equalsIgnoreCase(name)) continue;
if (StaffHeuristicsConfig.get().matches(info.name, info.displayName)) return true;
}
return false;
}
}

View file

@ -14,12 +14,16 @@ import java.util.*;
/**
* Provides sorted staff entries based on PlayerRelations and StaffTracker.
*/
public enum StaffData {
;
public final class StaffData {
public static List<StaffEntry> getEntries() {
public static final StaffData INSTANCE = new StaffData();
private StaffData() {
}
public List<StaffEntry> getEntries() {
Set<String> staffList = PlayerRelations.get().getStaff();
Map<UUID, StaffTracker.StaffInfo> map = StaffTracker.all();
Map<UUID, StaffTracker.StaffInfo> map = StaffTracker.INSTANCE.all();
List<StaffEntry> out = new ArrayList<>();
for (String rawName : staffList) {
@ -47,23 +51,6 @@ public enum StaffData {
));
}
for (StaffTracker.StaffInfo info : map.values()) {
if (info == null || info.name == null || info.name.isBlank()) continue;
if (containsIgnoreCase(staffList, info.name)) continue;
if (!StaffHeuristicsConfig.get().matches(info.name, info.displayName)) continue;
out.add(new StaffEntry(
info.name,
info.displayName != null
? info.displayName
: Component.literal(info.name),
info.status,
info.offlineSince,
info.lastEntityGone
));
}
out.sort(Comparator.comparing((StaffEntry e) -> sortPriority(e.status))
.thenComparing(e -> e.name.toLowerCase(Locale.ROOT)));
return out;
@ -91,14 +78,6 @@ public enum StaffData {
return null;
}
private static boolean containsIgnoreCase(Set<String> set, String value) {
if (set == null || value == null) return false;
for (String entry : set) {
if (entry != null && entry.equalsIgnoreCase(value)) return true;
}
return false;
}
public record StaffEntry(
String name,
Component displayName,

View file

@ -1,152 +0,0 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.relations;
import dev.loki.lovisual.config.ConfigNameProvider;
import dev.loki.lovisual.config.ConfigObject;
import dev.loki.lovisual.config.ConfigSerializer;
import dev.loki.lovisual.config.values.BooleanValue;
import dev.loki.lovisual.config.values.ConfigValue;
import dev.loki.lovisual.config.values.SetValue;
import dev.loki.lovisual.util.text.LegacyTextUtil;
import net.minecraft.network.chat.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public final class StaffHeuristicsConfig implements ConfigObject, ConfigNameProvider {
public static final StaffHeuristicsConfig INSTANCE = new StaffHeuristicsConfig();
private final BooleanValue enabled = new BooleanValue("enabled", false);
private final SetValue prefixes = new SetValue("prefixes");
private final SetValue suffixes = new SetValue("suffixes");
private final SetValue contains = new SetValue("contains");
private StaffHeuristicsConfig() {
ConfigSerializer.load(this);
}
public static StaffHeuristicsConfig get() {
return INSTANCE;
}
public boolean enabled() {
return enabled.get();
}
public void setEnabled(boolean value) {
enabled.set(value);
save();
}
public Set<String> prefixes() {
return prefixes.get();
}
public Set<String> suffixes() {
return suffixes.get();
}
public Set<String> contains() {
return contains.get();
}
public SetValue prefixesValue() {
return prefixes;
}
public SetValue suffixesValue() {
return suffixes;
}
public SetValue containsValue() {
return contains;
}
public boolean addPrefix(String value) {
return add(prefixes, value);
}
public boolean addSuffix(String value) {
return add(suffixes, value);
}
public boolean addContains(String value) {
return add(contains, value);
}
public boolean removePrefix(String value) {
return remove(prefixes, value);
}
public boolean removeSuffix(String value) {
return remove(suffixes, value);
}
public boolean removeContains(String value) {
return remove(contains, value);
}
public boolean matches(String name) {
return matches(name, null);
}
public boolean matches(String name, Component displayName) {
if (!enabled()) return false;
Component convertedDisplay = displayName == null ? null : LegacyTextUtil.convertLegacyCodes(displayName);
String display = convertedDisplay == null ? "" : convertedDisplay.getString();
return StaffHeuristicsMatcher.matches(name, display, prefixes.get(), suffixes.get(), contains.get());
}
public void save() {
ConfigSerializer.requestSave(this);
}
@Override
public String getConfigName() {
return "staffheuristics";
}
@Override
public List<ConfigValue<?>> getConfigValues() {
List<ConfigValue<?>> out = new ArrayList<>();
out.add(enabled);
out.add(prefixes);
out.add(suffixes);
out.add(contains);
return out;
}
private static boolean add(SetValue target, String raw) {
String value = cleanRule(raw);
if (value.isEmpty()) return false;
boolean changed = target.get().add(value);
if (changed) StaffHeuristicsConfig.get().save();
return changed;
}
private static boolean remove(SetValue target, String raw) {
if (raw == null || raw.isBlank()) return false;
String selected = null;
for (String entry : target.get()) {
if (entry != null && entry.equalsIgnoreCase(raw.trim())) {
selected = entry;
break;
}
}
boolean changed = selected != null && target.get().remove(selected);
if (changed) StaffHeuristicsConfig.get().save();
return changed;
}
public static String cleanRule(String raw) {
return raw == null ? "" : LegacyTextUtil.stripLegacy(raw).trim();
}
}

View file

@ -1,154 +0,0 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.relations;
import dev.loki.lovisual.util.text.LegacyTextUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
enum StaffHeuristicsMatcher {
;
static boolean matches(String name,
String displayName,
Set<String> prefixes,
Set<String> suffixes,
Set<String> contains) {
String plainName = normalizeText(name);
String display = normalizeText(displayName);
if (matchesText(plainName, prefixes, suffixes, contains)) return true;
if (display.isBlank()) return false;
if (matchesText(display, prefixes, suffixes, contains)) return true;
return matchesAffixesAroundName(plainName, display, prefixes, suffixes);
}
private static boolean matchesText(String text,
Set<String> prefixes,
Set<String> suffixes,
Set<String> contains) {
if (text.isBlank()) return false;
for (String prefix : prefixes) {
String rule = normalizeText(prefix);
if (!rule.isBlank() && startsWithRule(text, rule)) return true;
}
for (String suffix : suffixes) {
String rule = normalizeText(suffix);
if (!rule.isBlank() && endsWithRule(text, rule)) return true;
}
for (String needle : contains) {
String rule = normalizeText(needle);
if (!rule.isBlank() && containsRule(text, rule)) return true;
}
return false;
}
private static boolean matchesAffixesAroundName(String plainName,
String display,
Set<String> prefixes,
Set<String> suffixes) {
if (plainName.isBlank()) return false;
int from = 0;
while (from < display.length()) {
int idx = display.indexOf(plainName, from);
if (idx < 0) return false;
String beforeName = display.substring(0, idx);
String afterName = display.substring(idx + plainName.length());
if (matchesRegion(beforeName, prefixes) || matchesRegion(afterName, suffixes)) {
return true;
}
from = idx + Math.max(plainName.length(), 1);
}
return false;
}
private static boolean matchesRegion(String text, Set<String> rules) {
if (text.isBlank()) return false;
for (String raw : rules) {
String rule = normalizeText(raw);
if (!rule.isBlank() && containsRule(text, rule)) return true;
}
return false;
}
private static boolean startsWithRule(String text, String rule) {
if (text.startsWith(rule)) return true;
String compactRule = compactKey(rule);
return !compactRule.isBlank() && compactKey(text).startsWith(compactRule);
}
private static boolean endsWithRule(String text, String rule) {
if (text.endsWith(rule)) return true;
String compactRule = compactKey(rule);
return !compactRule.isBlank() && compactKey(text).endsWith(compactRule);
}
private static boolean containsRule(String text, String rule) {
return text.contains(rule) || containsCompactTokenSequence(text, rule);
}
private static boolean containsCompactTokenSequence(String text, String rule) {
String compactRule = compactKey(rule);
if (compactRule.isBlank()) return false;
List<String> tokens = compactTokens(text);
for (int start = 0; start < tokens.size(); start++) {
StringBuilder joined = new StringBuilder();
for (int i = start; i < tokens.size(); i++) {
joined.append(tokens.get(i));
if (joined.length() >= compactRule.length()) {
if (joined.toString().equals(compactRule)) return true;
break;
}
}
}
return false;
}
private static String compactKey(String text) {
if (text == null || text.isBlank()) return "";
StringBuilder out = new StringBuilder(text.length());
for (String token : compactTokens(text)) {
out.append(token);
}
return out.toString();
}
private static List<String> compactTokens(String text) {
List<String> out = new ArrayList<>();
if (text == null || text.isBlank()) return out;
StringBuilder token = new StringBuilder();
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
if (Character.isLetterOrDigit(ch) || ch == '_') {
token.append(ch);
continue;
}
if (!token.isEmpty()) {
out.add(token.toString());
token.setLength(0);
}
}
if (!token.isEmpty()) {
out.add(token.toString());
}
return out;
}
private static String normalizeText(String raw) {
if (raw == null || raw.isBlank()) return "";
return LegacyTextUtil.stripLegacy(raw).trim().toLowerCase(Locale.ROOT);
}
}

View file

@ -14,13 +14,17 @@ import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public enum StaffTracker {
;
public final class StaffTracker {
public static final StaffTracker INSTANCE = new StaffTracker();
private static final long TIMEOUT = 1500; // время ожидания TAB после исчезновения entity
private static final Map<UUID, StaffInfo> map = new HashMap<>();
private final Map<UUID, StaffInfo> map = new HashMap<>();
private static StaffInfo get(UUID id) {
private StaffTracker() {
}
private StaffInfo get(UUID id) {
return map.computeIfAbsent(id, k -> {
StaffInfo i = new StaffInfo();
i.uuid = k;
@ -29,7 +33,7 @@ public enum StaffTracker {
});
}
public static void onTabUpdate(UUID id, String name, Component displayName, Boolean listed, GameType gm) {
public void onTabUpdate(UUID id, String name, Component displayName, Boolean listed, GameType gm) {
StaffInfo i = get(id);
if (displayName != null) i.displayName = displayName;
@ -40,7 +44,7 @@ public enum StaffTracker {
recalc(i);
}
public static void onEntityAppear(UUID id, String name) {
public void onEntityAppear(UUID id, String name) {
StaffInfo i = get(id);
i.entityPresent = true;
@ -53,7 +57,7 @@ public enum StaffTracker {
// PUBLIC EVENTS
// ======================
public static void onEntityDisappear(UUID id) {
public void onEntityDisappear(UUID id) {
StaffInfo i = get(id);
i.entityPresent = false;
@ -62,7 +66,7 @@ public enum StaffTracker {
recalc(i);
}
private static void recalc(StaffInfo i) {
private void recalc(StaffInfo i) {
long now = System.currentTimeMillis();
// entity исчезла → показываем таймер ожидания TAB (WAITING)
@ -98,7 +102,7 @@ public enum StaffTracker {
i.status = Status.OFFLINE;
}
public static Map<UUID, StaffInfo> all() {
public Map<UUID, StaffInfo> all() {
return map;
}
@ -106,7 +110,7 @@ public enum StaffTracker {
// LOGIC
// ======================
public static void resetAll() {
public void resetAll() {
map.clear();
}

View file

@ -32,34 +32,38 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public enum BackdoorProtection {
;
public final class BackdoorProtection {
private static final Map<String, Set<String>> TRANSLATION_KEY_PACKS = new ConcurrentHashMap<>();
private static final Set<String> VANILLA_KEYBINDS = ConcurrentHashMap.newKeySet();
public static final BackdoorProtection INSTANCE = new BackdoorProtection();
private static volatile String currentServerAddress;
private static volatile boolean currentServerLocal;
private final Map<String, Set<String>> translationKeyPacks = new ConcurrentHashMap<>();
private final Set<String> vanillaKeybinds = ConcurrentHashMap.newKeySet();
public static void resetLoadedTranslations() {
TRANSLATION_KEY_PACKS.clear();
private volatile String currentServerAddress;
private volatile boolean currentServerLocal;
private BackdoorProtection() {
}
public static void registerTranslationKey(String key, String packId) {
public void resetLoadedTranslations() {
translationKeyPacks.clear();
}
public void registerTranslationKey(String key, String packId) {
if (key == null || key.isBlank() || packId == null || packId.isBlank()) return;
TRANSLATION_KEY_PACKS.computeIfAbsent(key, ignored -> ConcurrentHashMap.newKeySet()).add(packId);
translationKeyPacks.computeIfAbsent(key, ignored -> ConcurrentHashMap.newKeySet()).add(packId);
}
public static void resetVanillaKeybinds() {
VANILLA_KEYBINDS.clear();
public void resetVanillaKeybinds() {
vanillaKeybinds.clear();
}
public static void registerVanillaKeybind(String key) {
public void registerVanillaKeybind(String key) {
if (key == null || key.isBlank()) return;
VANILLA_KEYBINDS.add(key);
vanillaKeybinds.add(key);
}
public static void updateServerConnection(Connection connection) {
public void updateServerConnection(Connection connection) {
currentServerAddress = null;
currentServerLocal = true;
if (connection == null || connection.isMemoryConnection()) return;
@ -71,7 +75,7 @@ public enum BackdoorProtection {
currentServerLocal = isLocalAddressQuiet(host);
}
public static Component filterText(Component input) {
public Component filterText(Component input) {
if (input == null || !MainConfig.get().isBackdoorTranslationFilterEnabled()) return input;
ComponentContents content = filterContent(input.getContents());
@ -83,7 +87,7 @@ public enum BackdoorProtection {
return filtered;
}
private static ComponentContents filterContent(ComponentContents content) {
private ComponentContents filterContent(ComponentContents content) {
if (content instanceof FilteredTranslatableTextContent) {
return content;
}
@ -105,14 +109,14 @@ public enum BackdoorProtection {
return content;
}
public static boolean isTranslationAllowed(String key) {
public boolean isTranslationAllowed(String key) {
MainConfig cfg = MainConfig.get();
if (!cfg.isBackdoorTranslationFilterEnabled()) return true;
if (key == null || key.isBlank()) return true;
Set<String> packIds = TRANSLATION_KEY_PACKS.get(key);
Set<String> packIds = translationKeyPacks.get(key);
if (packIds == null || packIds.isEmpty()) {
return TRANSLATION_KEY_PACKS.isEmpty();
return translationKeyPacks.isEmpty();
}
Set<String> allowedPacks = cfg.getBackdoorAllowedTranslationPacks();
@ -124,14 +128,14 @@ public enum BackdoorProtection {
return false;
}
public static boolean isKeybindAllowed(String key) {
public boolean isKeybindAllowed(String key) {
MainConfig cfg = MainConfig.get();
if (!cfg.isBackdoorKeybindFilterEnabled()) return true;
if (key == null || key.isBlank()) return true;
return VANILLA_KEYBINDS.contains(key) || cfg.getBackdoorAllowedKeybinds().contains(key);
return vanillaKeybinds.contains(key) || cfg.getBackdoorAllowedKeybinds().contains(key);
}
public static void assertRemoteUrlAllowed(URL url) throws UnknownHostException {
public void assertRemoteUrlAllowed(URL url) throws UnknownHostException {
MainConfig cfg = MainConfig.get();
if (!cfg.isBackdoorLocalHttpGuardEnabled() || url == null) return;
String host = url.getHost();
@ -143,7 +147,7 @@ public enum BackdoorProtection {
throw new IllegalStateException("Blocked local/private HTTP request to " + host + " while connected to a remote server");
}
public static java.nio.file.Path isolatePackCachePath(java.nio.file.Path original) {
public java.nio.file.Path isolatePackCachePath(java.nio.file.Path original) {
MainConfig cfg = MainConfig.get();
if (!cfg.isBackdoorPackCacheIsolationEnabled() || original == null) return original;
Minecraft client = Minecraft.getInstance();

View file

@ -131,7 +131,7 @@ public abstract class ClientPacketListenerMixin {
Entity e = world.getEntity(id);
if (!(e instanceof Player player)) continue;
StaffTracker.onEntityDisappear(player.getUUID());
StaffTracker.INSTANCE.onEntityDisappear(player.getUUID());
}
}
@ -147,7 +147,7 @@ public abstract class ClientPacketListenerMixin {
Entity entity = world.getEntity(packet.getId());
if (!(entity instanceof Player player)) return;
StaffTracker.onEntityAppear(player.getUUID(), player.getGameProfile().name());
StaffTracker.INSTANCE.onEntityAppear(player.getUUID(), player.getGameProfile().name());
}
@Inject(method = "handlePlayerInfoUpdate", at = @At("TAIL"))
@ -174,7 +174,7 @@ public abstract class ClientPacketListenerMixin {
displayName = net.minecraft.network.chat.Component.literal(name);
}
StaffTracker.onTabUpdate(uuid, name, displayName, listed, gm);
StaffTracker.INSTANCE.onTabUpdate(uuid, name, displayName, listed, gm);
}
}
@ -184,10 +184,10 @@ public abstract class ClientPacketListenerMixin {
for (UUID id : packet.profileIds()) {
// убрали из TAB
StaffTracker.onTabUpdate(id, null, null, false, null);
StaffTracker.INSTANCE.onTabUpdate(id, null, null, false, null);
TotemPopCounter.onPlayerLogout(id);
// ВАЖНО — фикс VANISH после кика
StaffTracker.onEntityDisappear(id);
StaffTracker.INSTANCE.onEntityDisappear(id);
}
}

View file

@ -37,6 +37,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.visuals.BlockHighlight;
import dev.loki.lovisual.features.module.modules.visuals.ReimaginedVisual;
import dev.loki.lovisual.features.module.modules.visuals.ShaderSky;
import dev.loki.lovisual.features.module.modules.visuals.WorldTweaks;
import dev.loki.lovisual.mixins.accessors.GameRendererAccessor;
import dev.loki.lovisual.mixins.accessors.LevelRendererAccessor;
@ -232,7 +233,10 @@ public abstract class LevelRendererMixin {
}
ReimaginedVisual module = Modules.get(ReimaginedVisual.class);
if (module == null || !module.isEnabled()) return;
ShaderSky skyModule = Modules.get(ShaderSky.class);
boolean needsCustomSky = (module != null && module.isEnabled())
|| (skyModule != null && skyModule.isEnabled());
if (!needsCustomSky) return;
Minecraft client = Minecraft.getInstance();
net.minecraft.client.Camera camera = client != null && client.gameRenderer != null

View file

@ -295,7 +295,7 @@ public class MinecraftMixin implements MinecraftGameConfigHolder {
@Inject(method = "clearDownloadedResourcePacks", at = @At("HEAD"))
private void lovisual$onDisconnected(CallbackInfo ci) {
StaffTracker.resetAll();
StaffTracker.INSTANCE.resetAll();
AntiBotTracker.INSTANCE.reset();
}

View file

@ -43,7 +43,7 @@ public abstract class AbstractSignEditScreenMixin {
)
private void lovisual$backdoor$filterSignMessages(SignBlockEntity blockEntity, boolean front, boolean filtered, Component title, CallbackInfo ci) {
for (int line = 0; line < this.messages.length; line++) {
Component safeText = BackdoorProtection.filterText(this.text.getMessage(line, filtered));
Component safeText = BackdoorProtection.INSTANCE.filterText(this.text.getMessage(line, filtered));
this.messages[line] = safeText.getString();
}
}

View file

@ -31,11 +31,11 @@ public abstract class AnvilScreenMixin {
@WrapOperation(method = "onNameChanged", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/ItemStack;getHoverName()Lnet/minecraft/network/chat/Component;"))
private Component lovisual$backdoor$filterRenameName(ItemStack stack, Operation<Component> original) {
return BackdoorProtection.filterText(original.call(stack));
return BackdoorProtection.INSTANCE.filterText(original.call(stack));
}
@WrapOperation(method = "slotChanged", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/ItemStack;getHoverName()Lnet/minecraft/network/chat/Component;"))
private Component lovisual$backdoor$filterSlotUpdateName(ItemStack stack, Operation<Component> original) {
return BackdoorProtection.filterText(original.call(stack));
return BackdoorProtection.INSTANCE.filterText(original.call(stack));
}
}

View file

@ -37,6 +37,6 @@ public abstract class ClientLoginNetworkHandlerMixin {
@Inject(method = "handleLoginFinished", at = @At("HEAD"))
private void lovisual$backdoor$captureServerConnection(ClientboundLoginFinishedPacket packet, CallbackInfo ci) {
BackdoorProtection.updateServerConnection(connection);
BackdoorProtection.INSTANCE.updateServerConnection(connection);
}
}

View file

@ -39,9 +39,9 @@ public abstract class GameOptionsMixin {
@Inject(method = "<init>(Lnet/minecraft/client/Minecraft;Ljava/io/File;)V", at = @At("TAIL"))
private void lovisual$backdoor$registerVanillaKeybinds(Minecraft client, File optionsFile, CallbackInfo ci) {
BackdoorProtection.resetVanillaKeybinds();
BackdoorProtection.INSTANCE.resetVanillaKeybinds();
for (KeyMapping keyBinding : keyMappings) {
BackdoorProtection.registerVanillaKeybind(keyBinding.getName());
BackdoorProtection.INSTANCE.registerVanillaKeybind(keyBinding.getName());
}
}
}

View file

@ -41,7 +41,7 @@ public abstract class NetworkUtilsMixin {
@ModifyVariable(method = "downloadFile", at = @At("HEAD"), argsOnly = true, ordinal = 0)
private static Path lovisual$backdoor$isolatePackCache(Path original) {
return BackdoorProtection.isolatePackCachePath(original);
return BackdoorProtection.INSTANCE.isolatePackCachePath(original);
}
@WrapOperation(method = "downloadFile", at = @At(value = "INVOKE", target = "Ljava/net/HttpURLConnection;getInputStream()Ljava/io/InputStream;"))
@ -53,7 +53,7 @@ public abstract class NetworkUtilsMixin {
@Local LocalRef<HttpURLConnection> httpURLConnection
) throws IOException {
HttpURLConnection current = connection;
BackdoorProtection.assertRemoteUrlAllowed(current.getURL());
BackdoorProtection.INSTANCE.assertRemoteUrlAllowed(current.getURL());
current.setInstanceFollowRedirects(false);
int redirects = 0;
@ -64,7 +64,7 @@ public abstract class NetworkUtilsMixin {
}
URL next = resolveRedirect(current);
BackdoorProtection.assertRemoteUrlAllowed(next);
BackdoorProtection.INSTANCE.assertRemoteUrlAllowed(next);
current = (HttpURLConnection) next.openConnection(proxy);
current.setInstanceFollowRedirects(false);

View file

@ -38,7 +38,7 @@ public abstract class TranslationStorageMixin {
@Inject(method = "loadFrom(Lnet/minecraft/server/packs/resources/ResourceManager;Ljava/util/List;Z)Lnet/minecraft/client/resources/language/ClientLanguage;", at = @At("HEAD"))
private static void lovisual$backdoor$resetLoadedTranslations(ResourceManager resourceManager, List<String> definitions, boolean rightToLeft, CallbackInfoReturnable<ClientLanguage> cir) {
BackdoorProtection.resetLoadedTranslations();
BackdoorProtection.INSTANCE.resetLoadedTranslations();
}
@WrapOperation(
@ -53,7 +53,7 @@ public abstract class TranslationStorageMixin {
) {
String packId = resource.sourcePackId();
original.call(inputStream, (BiConsumer<String, String>) (key, value) -> {
BackdoorProtection.registerTranslationKey(key, packId);
BackdoorProtection.INSTANCE.registerTranslationKey(key, packId);
consumer.accept(key, value);
});
}

View file

@ -62,6 +62,8 @@ public enum LoVisualRenderPipelines {
public static final Identifier SHADER_POS_TEX_COLOR_SKY_FOG_ADDITIVE_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_tex_color_sky_fog_additive.frag");
public static final Identifier SHADER_POS_COLOR_SKYBOX_SHADER_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_color_skybox_shader.vert");
public static final Identifier SHADER_REIMAGINED_SKYBOX_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/reimagined_skybox.frag");
public static final Identifier SHADER_SHADER_SKY_THUNDER_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/shader_sky_thunder.frag");
public static final Identifier SHADER_SHADER_SKY_PULSAR_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/shader_sky_pulsar.frag");
public static final Identifier SHADER_POS_TEX_COLOR_RECT_PARAMS_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_tex_color_rect_params.vert");
public static final Identifier SHADER_POS_TEX_LOCAL_COLOR_RECT_PARAMS_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_tex_local_color_rect_params.vert");
public static final Identifier SHADER_POS_TEX_LOCAL_COLOR_RECT_PARAMS2_VERT = Identifier.fromNamespaceAndPath("lovisual", "shaders/pos_tex_local_color_rect_params2.vert");
@ -109,6 +111,13 @@ public enum LoVisualRenderPipelines {
public static final Identifier SHADER_HAND_METALLIC_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_metallic.frag");
public static final Identifier SHADER_UI_BLUR_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/ui_blur.frag");
public static final Identifier SHADER_HAND_GLASS_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_glass.frag");
public static final Identifier SHADER_HAND_CHAMS_SOLID_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_chams_solid.frag");
public static final Identifier SHADER_HAND_CHAMS_CHROMA_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_chams_chroma.frag");
public static final Identifier SHADER_HAND_CHAMS_STRIPES_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_chams_stripes.frag");
public static final Identifier SHADER_HAND_CHAMS_GLOW_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_chams_glow.frag");
public static final Identifier SHADER_HAND_CHAMS_SNOW_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_chams_snow.frag");
public static final Identifier SHADER_HAND_CHAMS_BALATRO_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_chams_balatro.frag");
public static final Identifier SHADER_HAND_CHAMS_BLEND_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/hand_chams_blend.frag");
public static final Identifier SHADER_SKY_SUN_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/sky_sun.frag");
public static final Identifier SHADER_MENU_BACKGROUND_WAVES_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/menu_background_waves.frag");
public static final Identifier SHADER_MENU_BACKGROUND_AURORA_FRAG = Identifier.fromNamespaceAndPath("lovisual", "shaders/menu_background_aurora.frag");
@ -970,6 +979,105 @@ public enum LoVisualRenderPipelines {
.withCull(false)
.build()
);
public static final RenderPipeline HAND_CHAMS_SOLID = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
.withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hand_chams_solid"))
.withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
.withVertexShader(SHADER_DAMAGE_TINT_VERT)
.withFragmentShader(SHADER_HAND_CHAMS_SOLID_FRAG)
.withSampler("u_Src")
.withSampler("u_Mask")
.withUniform("HandChams", UniformType.UNIFORM_BUFFER)
.withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
.withDepthWrite(false)
.withBlend(BlendFunction.TRANSLUCENT)
.withCull(false)
.build()
);
public static final RenderPipeline HAND_CHAMS_CHROMA = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
.withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hand_chams_chroma"))
.withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
.withVertexShader(SHADER_DAMAGE_TINT_VERT)
.withFragmentShader(SHADER_HAND_CHAMS_CHROMA_FRAG)
.withSampler("u_Src")
.withSampler("u_Mask")
.withUniform("HandChams", UniformType.UNIFORM_BUFFER)
.withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
.withDepthWrite(false)
.withBlend(BlendFunction.TRANSLUCENT)
.withCull(false)
.build()
);
public static final RenderPipeline HAND_CHAMS_STRIPES = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
.withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hand_chams_stripes"))
.withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
.withVertexShader(SHADER_DAMAGE_TINT_VERT)
.withFragmentShader(SHADER_HAND_CHAMS_STRIPES_FRAG)
.withSampler("u_Src")
.withSampler("u_Mask")
.withUniform("HandChams", UniformType.UNIFORM_BUFFER)
.withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
.withDepthWrite(false)
.withBlend(BlendFunction.TRANSLUCENT)
.withCull(false)
.build()
);
public static final RenderPipeline HAND_CHAMS_GLOW = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
.withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hand_chams_glow"))
.withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
.withVertexShader(SHADER_DAMAGE_TINT_VERT)
.withFragmentShader(SHADER_HAND_CHAMS_GLOW_FRAG)
.withSampler("u_Src")
.withSampler("u_Mask")
.withUniform("HandChams", UniformType.UNIFORM_BUFFER)
.withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
.withDepthWrite(false)
.withBlend(BlendFunction.TRANSLUCENT)
.withCull(false)
.build()
);
public static final RenderPipeline HAND_CHAMS_SNOW = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
.withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hand_chams_snow"))
.withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
.withVertexShader(SHADER_DAMAGE_TINT_VERT)
.withFragmentShader(SHADER_HAND_CHAMS_SNOW_FRAG)
.withSampler("u_Src")
.withSampler("u_Mask")
.withUniform("HandChams", UniformType.UNIFORM_BUFFER)
.withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
.withDepthWrite(false)
.withBlend(BlendFunction.TRANSLUCENT)
.withCull(false)
.build()
);
public static final RenderPipeline HAND_CHAMS_BALATRO = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
.withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hand_chams_balatro"))
.withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
.withVertexShader(SHADER_DAMAGE_TINT_VERT)
.withFragmentShader(SHADER_HAND_CHAMS_BALATRO_FRAG)
.withSampler("u_Src")
.withSampler("u_Mask")
.withUniform("HandChams", UniformType.UNIFORM_BUFFER)
.withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
.withDepthWrite(false)
.withBlend(BlendFunction.TRANSLUCENT)
.withCull(false)
.build()
);
public static final RenderPipeline HAND_CHAMS_BLEND = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
.withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/hand_chams_blend"))
.withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
.withVertexShader(SHADER_DAMAGE_TINT_VERT)
.withFragmentShader(SHADER_HAND_CHAMS_BLEND_FRAG)
.withSampler("u_Shader1")
.withSampler("u_Shader2")
.withSampler("u_Mask")
.withUniform("HandBlend", UniformType.UNIFORM_BUFFER)
.withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
.withDepthWrite(false)
.withBlend(BlendFunction.TRANSLUCENT)
.withCull(false)
.build()
);
private static final RenderPipeline.Snippet FOG_UNIFORMS = new ExtendedRenderPipelineBuilder()
.withUniform("Fog", UniformType.UNIFORM_BUFFER)
.buildSnippet();
@ -1020,6 +1128,36 @@ public enum LoVisualRenderPipelines {
.withCull(false)
.build()
);
/**
* Fullscreen procedural ShaderSky (Thunder) with sky/fog blending.
*/
public static final RenderPipeline WORLD_SHADER_SKY_THUNDER = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
.withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_shader_sky_thunder"))
.withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
.withVertexShader(SHADER_DAMAGE_TINT_VERT)
.withFragmentShader(SHADER_SHADER_SKY_THUNDER_FRAG)
.withUniform("ShaderSky", UniformType.UNIFORM_BUFFER)
.withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
.withDepthWrite(false)
.withBlend(BlendFunction.TRANSLUCENT)
.withCull(false)
.build()
);
/**
* Fullscreen procedural ShaderSky (Pulsar) with sky/fog blending.
*/
public static final RenderPipeline WORLD_SHADER_SKY_PULSAR = add(new ExtendedRenderPipelineBuilder(MESH_UNIFORMS)
.withLocation(Identifier.fromNamespaceAndPath("lovisual", "pipeline/world_shader_sky_pulsar"))
.withVertexFormat(LoVisualVertexFormats.POS2, com.mojang.blaze3d.PrimitiveTopology.TRIANGLES)
.withVertexShader(SHADER_DAMAGE_TINT_VERT)
.withFragmentShader(SHADER_SHADER_SKY_PULSAR_FRAG)
.withUniform("ShaderSky", UniformType.UNIFORM_BUFFER)
.withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
.withDepthWrite(false)
.withBlend(BlendFunction.TRANSLUCENT)
.withCull(false)
.build()
);
private static final RenderPipeline.Snippet UI_BATCH_UNIFORMS = new ExtendedRenderPipelineBuilder()
.withUniform("UIBatch", UniformType.UNIFORM_BUFFER)
.buildSnippet();

View file

@ -0,0 +1,50 @@
package dev.loki.lovisual.render.engine.uniform.impl;
import com.mojang.blaze3d.buffers.GpuBufferSlice;
import com.mojang.blaze3d.buffers.Std140Builder;
import com.mojang.blaze3d.buffers.Std140SizeCalculator;
import dev.loki.lovisual.render.engine.core.LoVisualRenderSystem;
import dev.loki.lovisual.render.engine.rhi.uniform.LoVisualUniformAllocator;
import java.nio.ByteBuffer;
/**
* UBO для dual-shader режима руки: смешивание двух шейдеров.
*/
public enum HandBlendUniforms {
;
public static final int SIZE = new Std140SizeCalculator()
.putVec4()
.get();
private static final Data DATA = new Data();
private static final String UNIFORM_NAME = "LoVisual - HandBlend UBO";
private static final int EXPECTED_WRITES_PER_FRAME = 1;
public static void update(int blendMode, float blendIntensity, float shader1Strength, float shader2Strength) {
DATA.blend[0] = blendMode;
DATA.blend[1] = Math.max(0.0f, Math.min(1.0f, blendIntensity));
DATA.blend[2] = Math.max(0.0f, Math.min(1.0f, shader1Strength));
DATA.blend[3] = Math.max(0.0f, Math.min(1.0f, shader2Strength));
LoVisualRenderSystem.uniforms().writeCached(UNIFORM_NAME, SIZE, EXPECTED_WRITES_PER_FRAME, 1, true, DATA);
}
public static GpuBufferSlice get() {
return LoVisualRenderSystem.uniforms().current(UNIFORM_NAME);
}
private static final class Data implements LoVisualUniformAllocator.UniformWriter {
private final float[] blend = new float[4];
@Override
public void write(ByteBuffer buffer) {
Std140Builder.intoBuffer(buffer)
.putFloat(blend[0]).putFloat(blend[1]).putFloat(blend[2]).putFloat(blend[3]);
}
@Override
public boolean equals(Object o) {
return false;
}
}
}

View file

@ -0,0 +1,127 @@
package dev.loki.lovisual.render.engine.uniform.impl;
import com.mojang.blaze3d.buffers.GpuBufferSlice;
import com.mojang.blaze3d.buffers.Std140Builder;
import com.mojang.blaze3d.buffers.Std140SizeCalculator;
import dev.loki.lovisual.render.engine.core.LoVisualRenderSystem;
import dev.loki.lovisual.render.engine.rhi.uniform.LoVisualUniformAllocator;
import java.nio.ByteBuffer;
/**
* Единый UBO для дополнительных режимов руки Chams (Snow/Chroma/Solid/Stripes/Glow/Balatro).
* Неиспользуемые режимом поля просто игнорируются шейдером.
*/
public enum HandChamsUniforms {
;
public static final int SIZE = new Std140SizeCalculator()
.putVec4() // u_View
.putVec4() // u_Mix
.putVec4() // u_ColorA
.putVec4() // u_ColorB
.putVec4() // u_ColorC
.putVec4() // u_ColorD
.putVec4() // u_Common
.putVec4() // u_Glow
.putVec4() // u_Flags
.get();
private static final Data DATA = new Data();
private static final String UNIFORM_NAME = "LoVisual - HandChams UBO";
private static final int EXPECTED_WRITES_PER_FRAME = 1;
public static void update(int colorA,
int colorB,
int colorC,
int colorD,
float resolutionX,
float resolutionY,
float time,
float effectAlpha,
float chromaSpeed,
float chromaSaturation,
float chromaBrightness,
float snowIntensity,
float snowSpeed,
float stripesWidth,
float stripesSpeed,
boolean fillGlow,
float glowRadius,
float glowPower,
float glowDispersion,
boolean hideHand) {
putRgb(DATA.colorA, colorA);
putRgb(DATA.colorB, colorB);
putRgb(DATA.colorC, colorC);
putRgb(DATA.colorD, colorD);
DATA.view[0] = resolutionX;
DATA.view[1] = resolutionY;
DATA.view[2] = time;
DATA.mix[0] = clamp01(effectAlpha);
DATA.mix[1] = chromaSpeed;
DATA.mix[2] = clamp01(chromaSaturation);
DATA.mix[3] = chromaBrightness;
DATA.common[0] = snowIntensity;
DATA.common[1] = snowSpeed;
DATA.common[2] = stripesWidth;
DATA.common[3] = stripesSpeed;
DATA.glow[0] = fillGlow ? 1.0f : 0.0f;
DATA.glow[1] = glowRadius;
DATA.glow[2] = glowPower;
DATA.glow[3] = glowDispersion;
DATA.flags[0] = hideHand ? 1.0f : 0.0f;
LoVisualRenderSystem.uniforms().writeCached(UNIFORM_NAME, SIZE, EXPECTED_WRITES_PER_FRAME, 1, true, DATA);
}
public static GpuBufferSlice get() {
return LoVisualRenderSystem.uniforms().current(UNIFORM_NAME);
}
private static void putRgb(float[] out, int argb) {
out[0] = ((argb >>> 16) & 0xFF) / 255f;
out[1] = ((argb >>> 8) & 0xFF) / 255f;
out[2] = (argb & 0xFF) / 255f;
out[3] = 1.0f;
}
private static float clamp01(float value) {
return Math.max(0.0f, Math.min(1.0f, value));
}
private static final class Data implements LoVisualUniformAllocator.UniformWriter {
private final float[] view = new float[4];
private final float[] mix = new float[4];
private final float[] colorA = new float[4];
private final float[] colorB = new float[4];
private final float[] colorC = new float[4];
private final float[] colorD = new float[4];
private final float[] common = new float[4];
private final float[] glow = new float[4];
private final float[] flags = new float[4];
@Override
public void write(ByteBuffer buffer) {
Std140Builder.intoBuffer(buffer)
.putFloat(view[0]).putFloat(view[1]).putFloat(view[2]).putFloat(view[3])
.putFloat(mix[0]).putFloat(mix[1]).putFloat(mix[2]).putFloat(mix[3])
.putFloat(colorA[0]).putFloat(colorA[1]).putFloat(colorA[2]).putFloat(colorA[3])
.putFloat(colorB[0]).putFloat(colorB[1]).putFloat(colorB[2]).putFloat(colorB[3])
.putFloat(colorC[0]).putFloat(colorC[1]).putFloat(colorC[2]).putFloat(colorC[3])
.putFloat(colorD[0]).putFloat(colorD[1]).putFloat(colorD[2]).putFloat(colorD[3])
.putFloat(common[0]).putFloat(common[1]).putFloat(common[2]).putFloat(common[3])
.putFloat(glow[0]).putFloat(glow[1]).putFloat(glow[2]).putFloat(glow[3])
.putFloat(flags[0]).putFloat(flags[1]).putFloat(flags[2]).putFloat(flags[3]);
}
@Override
public boolean equals(Object o) {
return false;
}
}
}

View file

@ -0,0 +1,96 @@
package dev.loki.lovisual.render.engine.uniform.impl;
import com.mojang.blaze3d.buffers.GpuBufferSlice;
import com.mojang.blaze3d.buffers.Std140Builder;
import com.mojang.blaze3d.buffers.Std140SizeCalculator;
import dev.loki.lovisual.render.engine.core.LoVisualRenderSystem;
import dev.loki.lovisual.render.engine.rhi.uniform.LoVisualUniformAllocator;
import java.nio.ByteBuffer;
/**
* UBO для модуля ShaderSky: общий набор параметров всех режимов неба.
*/
public enum ShaderSkyUniforms {
;
public static final int SIZE = new Std140SizeCalculator()
.putVec4() // u_Color
.putVec4() // u_Params
.putVec4() // u_View
.putVec4() // u_Cam
.putVec4() // u_Thunder
.get();
private static final Data DATA = new Data();
private static final String UNIFORM_NAME = "LoVisual - ShaderSky UBO";
private static final int EXPECTED_WRITES_PER_FRAME = 1;
public static void update(int colorRgb,
float alpha,
float time,
float speed,
float scale,
float intensity,
float resolutionX,
float resolutionY,
float yawRad,
float pitchRad,
float fov,
float thunderInterval,
float thunderChance,
float thunderGlow) {
DATA.color[0] = ((colorRgb >>> 16) & 0xFF) / 255f;
DATA.color[1] = ((colorRgb >>> 8) & 0xFF) / 255f;
DATA.color[2] = (colorRgb & 0xFF) / 255f;
DATA.color[3] = clamp01(alpha);
DATA.params[0] = time;
DATA.params[1] = speed;
DATA.params[2] = Math.max(0.05f, scale);
DATA.params[3] = intensity;
DATA.view[0] = resolutionX;
DATA.view[1] = resolutionY;
DATA.view[2] = fov;
DATA.cam[0] = yawRad;
DATA.cam[1] = pitchRad;
DATA.thunder[0] = Math.max(1.0f, thunderInterval);
DATA.thunder[1] = clamp01(thunderChance);
DATA.thunder[2] = thunderGlow;
LoVisualRenderSystem.uniforms().writeCached(UNIFORM_NAME, SIZE, EXPECTED_WRITES_PER_FRAME, 1, true, DATA);
}
public static GpuBufferSlice get() {
return LoVisualRenderSystem.uniforms().current(UNIFORM_NAME);
}
private static float clamp01(float value) {
return Math.max(0.0f, Math.min(1.0f, value));
}
private static final class Data implements LoVisualUniformAllocator.UniformWriter {
private final float[] color = new float[4];
private final float[] params = new float[4];
private final float[] view = new float[4];
private final float[] cam = new float[4];
private final float[] thunder = new float[4];
@Override
public void write(ByteBuffer buffer) {
Std140Builder.intoBuffer(buffer)
.putFloat(color[0]).putFloat(color[1]).putFloat(color[2]).putFloat(color[3])
.putFloat(params[0]).putFloat(params[1]).putFloat(params[2]).putFloat(params[3])
.putFloat(view[0]).putFloat(view[1]).putFloat(view[2]).putFloat(view[3])
.putFloat(cam[0]).putFloat(cam[1]).putFloat(cam[2]).putFloat(cam[3])
.putFloat(thunder[0]).putFloat(thunder[1]).putFloat(thunder[2]).putFloat(thunder[3]);
}
@Override
public boolean equals(Object o) {
return false;
}
}
}

View file

@ -24,7 +24,6 @@ import java.util.Deque;
public enum ScissorFunction {
;
private static final Minecraft mc = Minecraft.getInstance();
private static final Deque<ScissorRect> STACK = new ArrayDeque<>();
private static ScissorRect appliedScissor;
@ -37,6 +36,7 @@ public enum ScissorFunction {
float width,
float height
) {
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.getWindow() == null) return false;
float uiScale = ViewportContext.getUiScale();
@ -198,6 +198,7 @@ public enum ScissorFunction {
ScissorRect raw,
ScissorRect parent
) {
Minecraft mc = Minecraft.getInstance();
int fbw = mc != null && mc.getWindow() != null ? mc.getWindow().getWidth() : -1;
int fbh = mc != null && mc.getWindow() != null ? mc.getWindow().getHeight() : -1;
return new IllegalStateException(

View file

@ -8,6 +8,7 @@
package dev.loki.lovisual.render.sky;
import com.mojang.blaze3d.buffers.GpuBufferSlice;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.Camera;
@ -30,6 +31,8 @@ import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.visuals.Freecam;
import dev.loki.lovisual.features.module.modules.visuals.NoRender;
import dev.loki.lovisual.features.module.modules.visuals.ReimaginedVisual;
import dev.loki.lovisual.features.module.modules.visuals.ShaderSky;
import dev.loki.lovisual.features.theme.Theme;
import dev.loki.lovisual.mixins.accessors.GameRendererAccessor;
import dev.loki.lovisual.mixins.accessors.LocalPlayerAccessor;
import dev.loki.lovisual.render.engine.RenderState;
@ -39,6 +42,7 @@ import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer;
import dev.loki.lovisual.render.engine.renderer.MeshRenderer;
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
import dev.loki.lovisual.render.engine.uniform.impl.ShaderSkyUniforms;
import dev.loki.lovisual.render.engine.uniform.impl.SkySunUniforms;
import dev.loki.lovisual.render.engine.uniform.impl.SkyboxShaderUniforms;
@ -86,9 +90,13 @@ public enum CustomSkyboxRenderer {
float skyboxAlpha = Mth.lerp(twilightFade, 1.0f, MIN_TWILIGHT_SKYBOX_ALPHA);
ReimaginedVisual visual = Modules.get(ReimaginedVisual.class);
boolean useShaderSkybox = visual != null && visual.isShaderSkyboxEnabled();
ShaderSky skyModule = Modules.get(ShaderSky.class);
boolean useShaderSky = skyModule != null && skyModule.isEnabled();
boolean useShaderSkybox = !useShaderSky && visual != null && visual.isShaderSkyboxEnabled();
if (useShaderSkybox) {
if (useShaderSky) {
renderShaderSky(mc, mc.gameRenderer.mainRenderTarget(), camera, Theme.theme().accent() & 0x00FFFFFF, skyboxAlpha, tickDelta, fov, projection);
} else if (useShaderSkybox) {
renderShaderSkybox(mc, mc.gameRenderer.mainRenderTarget(), camera, fog, sky, skyboxAlpha, tickDelta, fov, projection);
}
if (shouldRenderShaderSun()) {
@ -172,6 +180,69 @@ public enum CustomSkyboxRenderer {
}
}
private static void renderShaderSky(Minecraft mc,
com.mojang.blaze3d.pipeline.RenderTarget framebuffer,
Camera camera,
int skyColor,
float alpha,
float tickDelta,
float fov,
Matrix4f worldProjection) {
if (alpha <= 0.001f) return;
ShaderSky skyModule = Modules.get(ShaderSky.class);
if (skyModule == null || !skyModule.isEnabled()) return;
RenderPipeline pipeline = pipelineForMode(skyModule.getMode());
if (pipeline == null) return;
float time = AnimationUtility.time(0.001f);
float width = mc.getWindow() != null ? mc.getWindow().getWidth() : 1.0f;
float height = mc.getWindow() != null ? mc.getWindow().getHeight() : 1.0f;
float yawRad = (float) Math.toRadians(-camera.yRot());
float pitchRad = (float) Math.toRadians(camera.xRot());
float shaderFov = extractProjectionFov(worldProjection, fov);
ShaderSkyUniforms.update(
skyColor,
alpha * skyModule.getAlpha(),
time,
skyModule.getSpeed(),
skyModule.getScale(),
skyModule.getIntensity(),
width,
height,
yawRad,
pitchRad,
shaderFov,
skyModule.getThunderInterval(),
skyModule.getThunderChance(),
skyModule.getThunderGlow()
);
FullScreenRenderer.ensureInit();
Matrix4fStack mv = RenderSystem.getModelViewStack();
mv.pushMatrix();
mv.identity();
MeshRenderer.setProjection(new Matrix4f());
try {
MeshRenderer.begin()
.attachments(framebuffer)
.pipeline(pipeline)
.mesh(FullScreenRenderer.vbo, FullScreenRenderer.ibo)
.uniform("ShaderSky", ShaderSkyUniforms.get())
.end();
} finally {
MeshRenderer.setProjection(worldProjection);
mv.popMatrix();
}
}
private static RenderPipeline pipelineForMode(String mode) {
return switch (mode) {
case "Pulsar" -> LoVisualRenderPipelines.WORLD_SHADER_SKY_PULSAR;
default -> LoVisualRenderPipelines.WORLD_SHADER_SKY_THUNDER;
};
}
private static float extractProjectionFov(Matrix4f projection, float fallbackFov) {
if (projection == null) {
return fallbackFov;

View file

@ -12,15 +12,16 @@ import net.minecraft.network.chat.Component;
public enum LegacyHudNotifier {
;
private static final Minecraft client = Minecraft.getInstance();
public static void show(String message) {
Minecraft client = Minecraft.getInstance();
if (client.gui != null) {
client.gui.hud.setOverlayMessage(Component.nullToEmpty(message), false);
}
}
public static void show(Component text) {
Minecraft client = Minecraft.getInstance();
if (client.gui != null) {
client.gui.hud.setOverlayMessage(text, false);
}

View file

@ -7,7 +7,7 @@
package dev.loki.lovisual.util.block;
import net.caffeinemc.mods.sodium.client.render.SodiumWorldRenderer;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.Minecraft;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.state.BlockState;
@ -23,24 +23,36 @@ public enum BlockObservationHub {
if (client == null || client.level == null || client.player == null) {
return;
}
SodiumWorldRenderer renderer = SodiumWorldRenderer.instanceNullable();
if (renderer == null) {
if (!FabricLoader.getInstance().isModLoaded("sodium")) {
return;
}
int renderRadius = (client.options.getEffectiveRenderDistance() + 1) * 16;
int radius = Math.max(1, Math.min(requestedRadius, renderRadius));
BlockPos center = client.player.blockPosition();
renderer.scheduleRebuildForBlockArea(
center.getX() - radius,
client.level.getMinY(),
center.getZ() - radius,
center.getX() + radius,
client.level.getMaxY(),
center.getZ() + radius,
true
);
try {
Class<?> cls = Class.forName("net.caffeinemc.mods.sodium.client.render.SodiumWorldRenderer");
Object renderer = cls.getMethod("instanceNullable").invoke(null);
if (renderer == null) {
return;
}
int renderRadius = (client.options.getEffectiveRenderDistance() + 1) * 16;
int radius = Math.max(1, Math.min(requestedRadius, renderRadius));
BlockPos center = client.player.blockPosition();
cls.getMethod(
"scheduleRebuildForBlockArea",
int.class, int.class, int.class, int.class, int.class, int.class, boolean.class
).invoke(
renderer,
center.getX() - radius,
client.level.getMinY(),
center.getZ() - radius,
center.getX() + radius,
client.level.getMaxY(),
center.getZ() + radius,
true
);
} catch (Throwable ignored) {
// Sodium отсутствует или изменился API — молча пропускаем
}
}
public static void observeSodiumBlock(int x, int y, int z, BlockState state) {

View file

@ -37,7 +37,6 @@ public enum ProtocolUtil {
public static final ClientProtocolVersion DEFAULT_PROTOCOL_VERSION =
new ClientProtocolVersion(SharedConstants.getCurrentVersion().name(), SharedConstants.getProtocolVersion());
private static final Minecraft MC = Minecraft.getInstance();
public static final boolean USES_VIAFABRICPLUS = detectViaFabricPlus();
private static boolean detectViaFabricPlus() {
@ -48,8 +47,9 @@ public enum ProtocolUtil {
try {
ViaFabricPlus.getImpl().registerOnChangeProtocolVersionCallback((oldVer, newVer) -> {
// Update the window title if available
MC.execute(MC::updateTitle);
// Обновляем заголовок окна лениво — только когда сработает колбэк
Minecraft client = Minecraft.getInstance();
client.execute(client::updateTitle);
});
} catch (Throwable t) {
DebugLog.error("Failed to register ViaFabricPlus protocol callback", t);

View file

@ -61,7 +61,7 @@ final class RemotePlayerObservation {
}
}
StaffTracker.StaffInfo info = StaffTracker.all().get(player.getUUID());
StaffTracker.StaffInfo info = StaffTracker.INSTANCE.all().get(player.getUUID());
if (info != null && info.gm != null) {
return info.gm;
}

View file

@ -21,7 +21,8 @@ import java.util.EnumMap;
public enum TargetManager {
;
private static final long DEFAULT_ATTACK_HOLD_MS = 700L;
// последняя цель удара запоминается на 10 секунд (боевая цель), пока не ударишь другого
private static final long DEFAULT_ATTACK_HOLD_MS = 10_000L;
private static final EnumMap<Source, TargetState> STATES = new EnumMap<>(Source.class);
private static LivingEntity current;
private static Source currentSource;
@ -60,56 +61,6 @@ public enum TargetManager {
}
}
public static void setForcedTarget(LivingEntity target) {
if (target == null) {
clear(Source.FORCE);
refreshCurrent();
return;
}
push(Source.FORCE, target, 0L);
refreshCurrent();
}
public static void setModuleTarget(LivingEntity target) {
if (target == null) {
clear(Source.MODULE);
refreshCurrent();
return;
}
push(Source.MODULE, target, 0L);
refreshCurrent();
}
public static void setAutoCrystalTarget(LivingEntity target) {
if (target == null) {
clear(Source.AUTO_CRYSTAL);
refreshCurrent();
return;
}
push(Source.AUTO_CRYSTAL, target, 0L);
refreshCurrent();
}
public static void setAutoAnchorTarget(LivingEntity target) {
if (target == null) {
clear(Source.AUTO_ANCHOR);
refreshCurrent();
return;
}
push(Source.AUTO_ANCHOR, target, 0L);
refreshCurrent();
}
public static void setAutoBedTarget(LivingEntity target) {
if (target == null) {
clear(Source.AUTO_BED);
refreshCurrent();
return;
}
push(Source.AUTO_BED, target, 0L);
refreshCurrent();
}
public static void setPredictionTarget(LivingEntity target) {
if (target == null) {
clear(Source.PREDICTION);
@ -120,16 +71,6 @@ public enum TargetManager {
refreshCurrent();
}
public static void setElytraTarget(LivingEntity target) {
if (target == null) {
clear(Source.ELYTRA);
refreshCurrent();
return;
}
push(Source.ELYTRA, target, 0L);
refreshCurrent();
}
public static void clear(Source source) {
if (source == null) return;
STATES.remove(source);
@ -192,7 +133,7 @@ public enum TargetManager {
}
private static TargetSelection selectCurrent(boolean includeCrosshair) {
for (Source source : new Source[]{Source.FORCE, Source.ELYTRA, Source.AUTO_CRYSTAL, Source.AUTO_ANCHOR, Source.AUTO_BED, Source.MODULE, Source.PREDICTION, Source.ATTACK, Source.CROSSHAIR}) {
for (Source source : new Source[]{Source.PREDICTION, Source.ATTACK, Source.CROSSHAIR}) {
if (!includeCrosshair && source == Source.CROSSHAIR) continue;
TargetState state = STATES.get(source);
if (state != null && TargetingUtil.isValidCombatTarget(state.entity)) {
@ -203,12 +144,6 @@ public enum TargetManager {
}
public enum Source {
FORCE,
ELYTRA,
AUTO_CRYSTAL,
AUTO_ANCHOR,
AUTO_BED,
MODULE,
PREDICTION,
ATTACK,
CROSSHAIR

View file

@ -0,0 +1,30 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.util.time;
import net.minecraft.client.Minecraft;
/**
* Игровое время (тики мира) в миллисекундах.
* Вне мира (меню/загрузка) — откат на стенное время, чтобы не было NPE.
*/
public final class GameClock {
public static final long TICK_MS = 50L;
private GameClock() {
}
public static long millis() {
Minecraft mc = Minecraft.getInstance();
if (mc != null && mc.level != null) {
return mc.level.getGameTime() * TICK_MS;
}
return System.currentTimeMillis();
}
}