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();
}
}

View file

@ -0,0 +1,88 @@
#version 330 core
in vec2 v_TexCoord;
out vec4 fragColor;
uniform sampler2D u_Src;
uniform sampler2D u_Mask;
layout (std140) uniform HandChams {
vec4 u_View; // x = resX, y = resY, z = time
vec4 u_Mix; // x = effectAlpha, y = chromaSpeed, z = chromaSaturation, w = chromaBrightness
vec4 u_ColorA; // rgb = colorA
vec4 u_ColorB; // rgb = colorB
vec4 u_ColorC; // rgb = colorC
vec4 u_ColorD; // rgb = colorD (r = contrast, g = lighting)
vec4 u_Common; // x = snowIntensity, y = snowSpeed, z = stripesWidth, w = stripesSpeed
vec4 u_Glow; // x = fillGlow, y = glowRadius, z = glowPower, w = glowDispersion
vec4 u_Flags; // x = hideHand
};
float handMaskAt(vec2 uv) {
vec4 m = texture(u_Mask, uv);
return max(m.a, max(m.r, max(m.g, m.b)));
}
#define SPIN_ROTATION -2.0
#define SPIN_SPEED 7.0
#define OFFSET vec2(0.0)
#define SPIN_AMOUNT 0.25
#define PIXEL_FILTER 745.0
#define SPIN_EASE 1.0
#define PI 3.14159265359
#define IS_ROTATE false
vec4 effect(vec2 screenSize, vec2 screen_coords, float contrast, float lighting) {
float pixel_size = length(screenSize.xy) / PIXEL_FILTER;
vec2 effectUv = (floor(screen_coords.xy*(1./pixel_size))*pixel_size - 0.5*screenSize.xy)/length(screenSize.xy) - OFFSET;
float uv_len = length(effectUv);
float speed = (SPIN_ROTATION*SPIN_EASE*0.2);
if(IS_ROTATE){
speed = u_View.z * speed;
}
speed += 302.2;
float new_pixel_angle = atan(effectUv.y, effectUv.x) + speed - SPIN_EASE*20.*(1.*SPIN_AMOUNT*uv_len + (1. - 1.*SPIN_AMOUNT));
vec2 mid = (screenSize.xy/length(screenSize.xy))/2.;
effectUv = (vec2((uv_len * cos(new_pixel_angle) + mid.x), (uv_len * sin(new_pixel_angle) + mid.y)) - mid);
effectUv *= 30.;
speed = u_View.z*(SPIN_SPEED);
vec2 uv2 = vec2(effectUv.x+effectUv.y);
for(int i=0; i < 5; i++) {
uv2 += sin(max(effectUv.x, effectUv.y)) + effectUv;
effectUv += 0.5*vec2(cos(5.1123314 + 0.353*uv2.y + speed*0.131121),sin(uv2.x - 0.113*speed));
effectUv -= 1.0*cos(effectUv.x + effectUv.y) - 1.0*sin(effectUv.x*0.711 - effectUv.y);
}
float contrast_mod = (0.25*contrast + 0.5*SPIN_AMOUNT + 1.2);
float paint_res = min(2., max(0.,length(effectUv)*(0.035)*contrast_mod));
float c1p = max(0.,1. - contrast_mod*abs(1.-paint_res));
float c2p = max(0.,1. - contrast_mod*abs(paint_res));
float c3p = 1. - min(1., c1p + c2p);
float light = (lighting - 0.2)*max(c1p*5. - 4., 0.) + lighting*max(c2p*5. - 4., 0.);
vec4 colour1 = vec4(u_ColorA.rgb, 1.0);
vec4 colour2 = vec4(u_ColorB.rgb, 1.0);
vec4 colour3 = vec4(u_ColorC.rgb, 1.0);
return (0.3/contrast)*colour1 + (1. - 0.3/contrast)*(colour1*c1p + colour2*c2p + vec4(c3p*colour3.rgb, c3p*colour1.a)) + light;
}
void main() {
vec4 base = texture(u_Src, v_TexCoord);
float mask = handMaskAt(v_TexCoord);
if (mask <= 0.02) {
fragColor = base;
return;
}
float contrast = clamp(u_ColorD.r, 0.1, 1.0) * 10.0;
float lighting = clamp(u_ColorD.g, 0.0, 1.0);
vec4 effectColor = effect(u_View.xy, v_TexCoord * u_View.xy, contrast, lighting);
vec3 finalColor = mix(base.rgb, effectColor.rgb, u_Mix.x * smoothstep(0.02, 0.3, mask));
fragColor = vec4(finalColor, 1.0);
}

View file

@ -0,0 +1,68 @@
#version 330 core
in vec2 v_TexCoord;
out vec4 fragColor;
uniform sampler2D u_Shader1;
uniform sampler2D u_Shader2;
uniform sampler2D u_Mask;
layout (std140) uniform HandBlend {
vec4 u_Blend; // x = blendMode (0=Mix,1=Add,2=Multiply,3=Screen,4=Overlay,5=Difference), y = blendIntensity, z = shader1Strength, w = shader2Strength
};
float maskAt(vec2 uv) {
vec4 m = texture(u_Mask, uv);
return max(m.a, max(m.r, max(m.g, m.b)));
}
vec3 blendAdd(vec3 base, vec3 blend) {
return min(base + blend, vec3(1.0));
}
vec3 blendScreen(vec3 base, vec3 blend) {
return vec3(1.0) - (vec3(1.0) - base) * (vec3(1.0) - blend);
}
vec3 blendOverlay(vec3 base, vec3 blend) {
vec3 result;
result.r = base.r < 0.5 ? (2.0 * base.r * blend.r) : (1.0 - 2.0 * (1.0 - base.r) * (1.0 - blend.r));
result.g = base.g < 0.5 ? (2.0 * base.g * blend.g) : (1.0 - 2.0 * (1.0 - base.g) * (1.0 - blend.g));
result.b = base.b < 0.5 ? (2.0 * base.b * blend.b) : (1.0 - 2.0 * (1.0 - base.b) * (1.0 - blend.b));
return result;
}
void main() {
vec4 shader1Color = texture(u_Shader1, v_TexCoord);
vec4 shader2Color = texture(u_Shader2, v_TexCoord);
float mask = maskAt(v_TexCoord);
if (mask <= 0.01) {
fragColor = shader1Color;
return;
}
vec3 color1 = shader1Color.rgb * u_Blend.z;
vec3 color2 = shader2Color.rgb * u_Blend.w;
float intensity = u_Blend.y;
vec3 blendedColor;
int mode = int(u_Blend.x);
if (mode == 0) {
blendedColor = mix(color1, color2, intensity);
} else if (mode == 1) {
blendedColor = mix(color1, blendAdd(color1, color2), intensity);
} else if (mode == 2) {
blendedColor = mix(color1, color1 * color2, intensity);
} else if (mode == 3) {
blendedColor = mix(color1, blendScreen(color1, color2), intensity);
} else if (mode == 4) {
blendedColor = mix(color1, blendOverlay(color1, color2), intensity);
} else if (mode == 5) {
blendedColor = mix(color1, abs(color1 - color2), intensity);
} else {
blendedColor = color1;
}
fragColor = vec4(blendedColor, 1.0);
}

View file

@ -0,0 +1,55 @@
#version 330 core
in vec2 v_TexCoord;
out vec4 fragColor;
uniform sampler2D u_Src;
uniform sampler2D u_Mask;
layout (std140) uniform HandChams {
vec4 u_View; // x = resX, y = resY, z = time
vec4 u_Mix; // x = effectAlpha, y = chromaSpeed, z = chromaSaturation, w = chromaBrightness
vec4 u_ColorA; // rgb = colorA
vec4 u_ColorB; // rgb = colorB
vec4 u_ColorC; // rgb = colorC
vec4 u_ColorD; // rgb = colorD
vec4 u_Common; // x = snowIntensity, y = snowSpeed, z = stripesWidth, w = stripesSpeed
vec4 u_Glow; // x = fillGlow, y = glowRadius, z = glowPower, w = glowDispersion
vec4 u_Flags; // x = hideHand
};
float handMaskAt(vec2 uv) {
vec4 m = texture(u_Mask, uv);
return max(m.a, max(m.r, max(m.g, m.b)));
}
vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
vec3 createChromaEffect(vec2 coord, float t) {
float hue = fract(coord.y * 0.5 + coord.x * 0.3 + t * u_Mix.y);
float wave = sin(coord.y * 10.0 + t * 3.0) * 0.05;
hue += wave;
return hsv2rgb(vec3(hue, u_Mix.z, u_Mix.w));
}
void main() {
vec4 base = texture(u_Src, v_TexCoord);
float mask = handMaskAt(v_TexCoord);
if (mask <= 0.02) {
fragColor = base;
return;
}
float time = u_View.z;
float maskGain = smoothstep(0.02, 0.3, mask);
vec3 chromaColor = createChromaEffect(v_TexCoord, time);
float pulse = 0.9 + sin(time * 4.0) * 0.1;
chromaColor *= pulse;
vec3 finalColor = mix(base.rgb, chromaColor, u_Mix.x * maskGain);
fragColor = vec4(finalColor, 1.0);
}

View file

@ -0,0 +1,97 @@
#version 330 core
in vec2 v_TexCoord;
out vec4 fragColor;
uniform sampler2D u_Src;
uniform sampler2D u_Mask;
layout (std140) uniform HandChams {
vec4 u_View; // x = resX, y = resY, z = time
vec4 u_Mix; // x = effectAlpha, y = chromaSpeed, z = chromaSaturation, w = chromaBrightness
vec4 u_ColorA; // rgb = colorA
vec4 u_ColorB; // rgb = colorB
vec4 u_ColorC; // rgb = colorC
vec4 u_ColorD; // rgb = colorD
vec4 u_Common; // x = snowIntensity, y = snowSpeed, z = stripesWidth, w = stripesSpeed
vec4 u_Glow; // x = fillGlow, y = glowRadius, z = glowPower, w = glowDispersion
vec4 u_Flags; // x = hideHand
};
float handMaskAt(vec2 uv) {
vec4 m = texture(u_Mask, uv);
return max(m.a, max(m.r, max(m.g, m.b)));
}
void main() {
vec4 base = texture(u_Src, v_TexCoord);
float mask = handMaskAt(v_TexCoord);
if (mask <= 0.02 && u_Glow.w <= 1.0) {
fragColor = base;
return;
}
float time = u_View.z;
float maskGain = smoothstep(0.02, 0.3, mask);
vec2 texelSize = 1.0 / max(u_View.xy, vec2(1.0));
// Маска: 0 = фон, >0 = рука. Инвертируем в depth-семантику (1 = фон).
float centerDepth = 1.0 - mask;
float edgeStrength = 0.0;
float outwardGlow = 0.0;
int disperseRadius = int(clamp(u_Glow.w, 1.0, 12.0));
for (int y = -disperseRadius; y <= disperseRadius; y++) {
for (int x = -disperseRadius; x <= disperseRadius; x++) {
if (x == 0 && y == 0) continue;
vec2 offset = vec2(x, y) * texelSize;
float neighborMask = handMaskAt(v_TexCoord + offset);
float neighborDepth = 1.0 - neighborMask;
float dist = length(vec2(x, y));
// Сосед — фон, а мы — рука: край руки
if (neighborDepth > 0.9 && centerDepth < 0.9) {
edgeStrength += (1.0 / (dist + 0.1));
}
// Мы — фон рядом с рукой: внешнее свечение
if (centerDepth > 0.9 && neighborDepth < 0.9) {
float falloff = 1.0 - (dist / float(disperseRadius));
falloff = pow(max(falloff, 0.0), 2.5);
outwardGlow += falloff * 0.2;
}
}
}
edgeStrength = clamp(edgeStrength * 0.15, 0.0, 1.0) * maskGain;
outwardGlow = clamp(outwardGlow, 0.0, 1.0);
float pulse = 0.9 + sin(time * 2.0) * 0.1;
float fill = u_Glow.x;
float glowRadius = max(u_Glow.y, 0.01);
float glowPower = max(u_Glow.z, 0.0);
// Фон рядом с рукой: внешнее свечение
if (centerDepth > 0.9 && outwardGlow > 0.01) {
vec3 dispersedGlow = u_ColorC.rgb * outwardGlow * glowPower * pulse * glowRadius * 10.0;
fragColor = vec4(dispersedGlow, outwardGlow * 0.8);
return;
}
vec3 finalColor;
if (fill > 0.5) {
vec3 handGlow = u_ColorC.rgb * (glowRadius * 10.0);
vec3 edgeGlow = u_ColorC.rgb * edgeStrength * glowPower * pulse * 2.5;
float glowMix = u_Flags.x > 0.5 ? 1.0 : 0.75;
finalColor = mix(base.rgb, handGlow + edgeGlow, glowMix);
} else {
vec3 edgeGlow = u_ColorC.rgb * edgeStrength * glowPower * pulse * glowRadius * 25.0;
finalColor = base.rgb + edgeGlow;
}
fragColor = vec4(finalColor, 1.0);
}

View file

@ -0,0 +1,164 @@
#version 330 core
in vec2 v_TexCoord;
out vec4 fragColor;
uniform sampler2D u_Src;
uniform sampler2D u_Mask;
layout (std140) uniform HandChams {
vec4 u_View; // x = resX, y = resY, z = time
vec4 u_Mix; // x = effectAlpha, y = chromaSpeed, z = chromaSaturation, w = chromaBrightness
vec4 u_ColorA; // rgb = colorA
vec4 u_ColorB; // rgb = colorB
vec4 u_ColorC; // rgb = colorC
vec4 u_ColorD; // rgb = colorD
vec4 u_Common; // x = snowIntensity, y = snowSpeed, z = stripesWidth, w = stripesSpeed
vec4 u_Glow; // x = fillGlow, y = glowRadius, z = glowPower, w = glowDispersion
vec4 u_Flags; // x = hideHand
};
float handMaskAt(vec2 uv) {
vec4 m = texture(u_Mask, uv);
return max(m.a, max(m.r, max(m.g, m.b)));
}
#define pi 3.14159265359
const int nbFlakes = 500;
const vec3 flakedomain = vec3(2.5, 3.0, 2.5);
const float flakeMinSpeed = 0.8;
const float flakeMaxSpeed = 2.0;
const float flakeMinSinVariation = 0.015;
const float flakeMaxSinVariation = 0.035;
const float flakeMinFreq = 3.0;
const float flakeMaxFreq = 8.0;
const vec2 flakeWindFact = vec2(0.25, 0.05);
const float starNbBranches = 6.0;
const float starPow = 1.5;
const float starStrength = 1.0;
vec2 rotateVec(vec2 vect, float angle) {
vec2 rv;
rv.x = vect.x * cos(angle) - vect.y * sin(angle);
rv.y = vect.x * sin(angle) + vect.y * cos(angle);
return rv;
}
float hash(float n) {
return fract(sin(n) * 753.5453123);
}
float rand(float minVal, float maxVal, float seed) {
return minVal + (maxVal - minVal) * hash(seed);
}
vec3 getFlakePosition(int flakeNr, float t) {
float fn = float(flakeNr);
float s = rand(flakeMinSpeed, flakeMaxSpeed, fn * 348.0 + 173.0) * u_Common.y;
float posY = mod(-(t + 15.0 * hash(fn * 1613.0 + 1354.0)) * s, flakedomain.y * 2.0) - flakedomain.y;
float posX = rand(-flakedomain.x, flakedomain.x, fn * 743.0 + 514.0) + posY * flakeWindFact.x;
float posZ = rand(-flakedomain.z, flakedomain.z, fn * 284.0 + 483.0) + posY * flakeWindFact.y;
float sinvar = rand(flakeMinSinVariation, flakeMaxSinVariation, fn * 842.0 + 951.0);
float sinfreq = rand(flakeMinFreq, flakeMaxFreq, fn * 348.0 + 173.0);
float dd = hash(fn * 235.0 + 934.0);
posX += sinvar * sin(t * sinfreq) * dd;
posZ += sinvar * sin(t * sinfreq) * sqrt(1.0 - dd * dd);
return vec3(posX, posY, posZ);
}
float nppow(float x, float p) {
return sign(x) * pow(abs(x), p);
}
float getSnowProfile(float val, float dist, vec3 fpos, vec3 ray, vec3 campos, int flakeNr) {
float val2 = -log(1.0 - val);
if (dist < 1.5) {
vec3 v3 = (fpos - campos) - dot((fpos - campos), ray) * ray;
vec3 vx = vec3(1.0, 0.0, 0.0);
vx.xy = rotateVec(vx.xy, 2.0 * float(flakeNr) * 152.5 + u_View.z * 0.4);
vx = normalize(vx - dot(vx, ray) * ray);
vec3 vy = vec3(ray.y * vx.z - ray.z * vx.y, ray.z * vx.x - ray.x * vx.z, ray.x * vx.y - ray.y * vx.x);
float a = atan(dot(v3, vx), dot(v3, vy));
float spp = 1.0 + starStrength * nppow(sin(a * starNbBranches), starPow);
val2 += 1.3 * spp * pow(smoothstep(1.6, 0.1, dist), 2.0);
}
float delta = 1.5 - 0.9 / pow(dist + 1.0, 0.3);
float midpoint = 10.0 / pow(dist + 0.1, 0.3);
float pr = smoothstep(midpoint - delta * 0.5, midpoint + delta * 0.5, val2);
float d = 1.0 - pow(abs(1.0 - 2.0 * pr), 2.0);
float f = 1.3 / pow(dist + 0.8, 2.5);
if (val2 < 8.0) {
pr += 32.0 * pow(f, 1.5) * max(0.0, dist - 2.0) * d * (0.5 + sin(val2 * 230.0 / (3.8 + dist) - midpoint * 90.0) * 0.5);
}
return pr * f;
}
vec3 getFlakes(vec3 ray, vec3 campos) {
vec3 rc1 = vec3(0.0);
vec3 rc2 = vec3(0.0);
vec3 fpos;
float lp;
for (int l = 0; l < nbFlakes; l++) {
fpos = getFlakePosition(l, u_View.z);
float val = max(0.0, dot(ray, normalize(fpos - campos)));
if (val > 0.996) {
vec3 camtarget = vec3(0.0, 0.0, 0.0);
float dist1 = distance(camtarget, fpos);
float dist2 = distance(campos, fpos);
float dist = max(5.2 * pow(dist1 / dist2, 1.7), 0.32);
lp = getSnowProfile(val, dist, fpos, ray, campos, l);
const float fogdens = 0.08;
lp *= clamp(exp(-pow(fogdens * dist2, 2.0)), 0.0, 1.0);
lp *= smoothstep(-flakedomain.y, -flakedomain.y * 0.75, fpos.y);
lp *= smoothstep(flakedomain.y, flakedomain.y * 0.75, fpos.y);
rc1 += clamp(normalize(mix(u_ColorA.rgb, vec3(1.0), 0.55 * lp)) * lp, 0.0, 1.0);
rc2 = max(rc2, clamp(normalize(mix(u_ColorA.rgb, vec3(1.0), 0.55 * lp)) * lp, 0.0, 1.0));
}
}
return mix(rc1, rc2, 0.7);
}
vec3 getCameraRayDir(vec2 vWindow) {
float fov = 3.8;
vec3 vForward = normalize(vec3(0.0, 0.0, -1.0));
vec3 vRight = normalize(cross(vec3(0.0, 1.0, 0.0), vForward));
vec3 vUp = normalize(cross(vForward, vRight));
return normalize(vWindow.x * vRight + vWindow.y * vUp + vForward * fov);
}
void main() {
vec4 base = texture(u_Src, v_TexCoord);
float mask = handMaskAt(v_TexCoord);
if (mask <= 0.02) {
fragColor = base;
return;
}
vec2 screenUv = v_TexCoord * 2.0 - 1.0;
screenUv.x *= u_View.x / max(u_View.y, 1.0);
vec3 ray = getCameraRayDir(screenUv);
vec3 campos = vec3(0.0, 0.0, 3.5);
vec3 flakes = getFlakes(ray, campos);
flakes *= u_Common.x * 1.8 * smoothstep(0.02, 0.3, mask);
vec3 finalColor = u_Flags.x > 0.5 ? flakes : base.rgb + flakes;
fragColor = vec4(finalColor, 1.0);
}

View file

@ -0,0 +1,48 @@
#version 330 core
in vec2 v_TexCoord;
out vec4 fragColor;
uniform sampler2D u_Src;
uniform sampler2D u_Mask;
layout (std140) uniform HandChams {
vec4 u_View; // x = resX, y = resY, z = time
vec4 u_Mix; // x = effectAlpha, y = chromaSpeed, z = chromaSaturation, w = chromaBrightness
vec4 u_ColorA; // rgb = colorA
vec4 u_ColorB; // rgb = colorB
vec4 u_ColorC; // rgb = colorC
vec4 u_ColorD; // rgb = colorD
vec4 u_Common; // x = snowIntensity, y = snowSpeed, z = stripesWidth, w = stripesSpeed
vec4 u_Glow; // x = fillGlow, y = glowRadius, z = glowPower, w = glowDispersion
vec4 u_Flags; // x = hideHand
};
float handMaskAt(vec2 uv) {
vec4 m = texture(u_Mask, uv);
return max(m.a, max(m.r, max(m.g, m.b)));
}
vec3 createVerticalGradient(vec2 coord, vec3 color1, vec3 color2, float t) {
float factor = coord.y + t;
factor = sin(factor * 3.14159 * 2.0) * 0.5 + 0.5;
return mix(color1, color2, factor);
}
void main() {
vec4 base = texture(u_Src, v_TexCoord);
float mask = handMaskAt(v_TexCoord);
if (mask <= 0.02) {
fragColor = base;
return;
}
float time = u_View.z;
float maskGain = smoothstep(0.02, 0.3, mask);
vec3 gradientColor = createVerticalGradient(v_TexCoord, u_ColorA.rgb, u_ColorB.rgb, time);
float brightness = 0.85 + sin(time * 6.28318) * 0.15;
gradientColor *= brightness;
vec3 finalColor = mix(base.rgb, gradientColor, u_Mix.x * maskGain);
fragColor = vec4(finalColor, 1.0);
}

View file

@ -0,0 +1,49 @@
#version 330 core
in vec2 v_TexCoord;
out vec4 fragColor;
uniform sampler2D u_Src;
uniform sampler2D u_Mask;
layout (std140) uniform HandChams {
vec4 u_View; // x = resX, y = resY, z = time
vec4 u_Mix; // x = effectAlpha, y = chromaSpeed, z = chromaSaturation, w = chromaBrightness
vec4 u_ColorA; // rgb = colorA
vec4 u_ColorB; // rgb = colorB
vec4 u_ColorC; // rgb = colorC
vec4 u_ColorD; // rgb = colorD
vec4 u_Common; // x = snowIntensity, y = snowSpeed, z = stripesWidth, w = stripesSpeed
vec4 u_Glow; // x = fillGlow, y = glowRadius, z = glowPower, w = glowDispersion
vec4 u_Flags; // x = hideHand
};
float handMaskAt(vec2 uv) {
vec4 m = texture(u_Mask, uv);
return max(m.a, max(m.r, max(m.g, m.b)));
}
void main() {
vec4 base = texture(u_Src, v_TexCoord);
float mask = handMaskAt(v_TexCoord);
if (mask <= 0.02) {
fragColor = base;
return;
}
vec2 effectUv = v_TexCoord;
effectUv.x *= u_View.x / max(u_View.y, 1.0);
// Плавные анимированные диагональные полосы с мягкими краями
float angle = 0.65;
float d = (effectUv.x * cos(angle) + effectUv.y * sin(angle)) + u_View.z * u_Common.w;
float stripe = abs(sin(d * 3.14159265 / max(u_Common.z, 0.001)));
stripe = pow(stripe, 1.5);
vec3 col = mix(u_ColorB.rgb, u_ColorA.rgb, stripe);
col += u_ColorA.rgb * (0.12 + 0.10 * sin(d * 6.0 + u_View.z * 2.0));
float strength = u_Mix.x * (0.35 + 0.65 * stripe) * smoothstep(0.02, 0.3, mask);
vec3 finalColor = mix(base.rgb, col, strength);
fragColor = vec4(finalColor, 1.0);
}

View file

@ -0,0 +1,182 @@
#version 330 core
layout (std140) uniform ShaderSky {
vec4 u_Color; // rgb = tint, a = alpha
vec4 u_Params; // x = time, y = speed, z = scale, w = intensity
vec4 u_View; // x/y = resolution, z = fov
vec4 u_Cam; // x = yaw, y = pitch
vec4 u_Thunder; // x = interval, y = chance, z = glow
};
out vec4 color;
mat3 rotX(float a) {
float c = cos(a), s = sin(a);
return mat3(1.0, 0.0, 0.0,
0.0, c, s,
0.0, -s, c);
}
mat3 rotY(float a) {
float c = cos(a), s = sin(a);
return mat3( c, 0.0, s,
0.0, 1.0, 0.0,
-s, 0.0, c);
}
// Optimized fast hash function
float hash(vec3 p) {
return fract(sin(dot(p, vec3(127.1, 311.7, 74.7))) * 43758.5453123);
}
float hash(float n) {
return fract(sin(n) * 43758.5453123);
}
// Vectorized 3D noise for FBM nebulae
float noise(vec3 p) {
vec3 i = floor(p);
vec3 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float n000 = hash(i + vec3(0.0, 0.0, 0.0));
float n100 = hash(i + vec3(1.0, 0.0, 0.0));
float n010 = hash(i + vec3(0.0, 1.0, 0.0));
float n110 = hash(i + vec3(1.0, 1.0, 0.0));
float n001 = hash(i + vec3(0.0, 0.0, 1.0));
float n101 = hash(i + vec3(1.0, 0.0, 1.0));
float n011 = hash(i + vec3(0.0, 1.0, 1.0));
float n111 = hash(i + vec3(1.0, 1.0, 1.0));
vec4 a = mix(vec4(n000, n010, n001, n011),
vec4(n100, n110, n101, n111), f.x);
vec2 b = mix(a.xz, a.yw, f.y);
return mix(b.x, b.y, f.z);
}
float fbm(vec3 p) {
float v = 0.0;
float a = 0.5;
vec3 shift = vec3(100.0);
for (int i = 0; i < 4; ++i) {
v += a * noise(p);
p = p * 2.0 + shift;
a *= 0.5;
}
return v;
}
// Realistic twinkling star field with varied star colors
vec3 starField(vec3 rd) {
// Reduced density (130.0 grid) and higher threshold (0.992) for sharper background stars
vec3 gridPos = floor(rd * 130.0);
float starHash = hash(gridPos);
vec3 starColor = vec3(0.0);
if (starHash > 0.992) {
vec3 starOffset = vec3(hash(gridPos + 1.1), hash(gridPos + 2.2), hash(gridPos + 3.3)) - 0.5;
vec3 starPos = (gridPos + 0.5 + starOffset * 0.85) / 130.0;
float d = length(rd - normalize(starPos));
// Twinkling effect
float twinkle = 0.55 + 0.45 * sin(u_Params.x * 3.5 + starHash * 120.0);
float starIntensity = exp(-d * 600.0) * twinkle * (0.3 + 0.7 * hash(gridPos + 5.5));
// Varied color temperatures for stars
vec3 col = vec3(1.0);
float colorHash = hash(gridPos + 7.7);
if (colorHash < 0.22) {
col = vec3(0.65, 0.8, 1.0); // Hot blue star
} else if (colorHash < 0.38) {
col = vec3(1.0, 0.82, 0.6); // Warm orange star
} else if (colorHash < 0.43) {
col = vec3(1.0, 0.55, 0.55); // Cool red star
}
starColor = col * starIntensity;
}
return starColor;
}
void main() {
vec2 uv = gl_FragCoord.xy / u_View.xy;
vec2 sp = uv * 2.0 - 1.0;
float aspect = u_View.x / u_View.y;
float tanV = tan(radians(u_View.z) * 0.5);
vec3 rayV = normalize(vec3(sp.x * tanV * aspect, sp.y * tanV, 1.0));
vec3 rayW = rotY(u_Cam.x) * rotX(u_Cam.y) * rayV;
// 1. Cosmic background with stars and detailed dust lane nebulae
float n1 = fbm(rayW * u_Params.z * 0.4 + vec3(u_Params.x * 0.003, 0.0, 0.0));
float n2 = fbm(rayW * u_Params.z * 0.9 - vec3(0.0, u_Params.x * 0.002, 0.0));
// Glowing active gas clouds
vec3 gasColor = mix(vec3(0.001, 0.003, 0.012), vec3(0.035, 0.01, 0.055), n1);
gasColor = mix(gasColor, vec3(0.008, 0.025, 0.035), n2 * n1);
// Dark silhouette dust lanes blocking the gas
float dust = smoothstep(0.3, 0.7, fbm(rayW * u_Params.z * 1.2 + vec3(0.1)));
vec3 skyBackground = mix(gasColor, vec3(0.0005, 0.0005, 0.0015), dust * 0.85);
vec3 col = skyBackground + starField(rayW);
// 2. Precessing Pulsar math
vec3 pulsarCenter = normalize(vec3(0.55, 0.65, -0.45));
// Only render pulsar on the front-facing hemisphere
float frontMask = smoothstep(0.0, 0.4, dot(rayW, pulsarCenter));
if (frontMask > 0.0) {
// Orthogonal tangents relative to the pulsar direction
vec3 tangent1 = normalize(cross(pulsarCenter, vec3(0.0, 1.0, 0.0)));
vec3 tangent2 = cross(tangent1, pulsarCenter);
// Local 2D coordinates centered on the pulsar core
vec2 p2d = vec2(dot(rayW, tangent1), dot(rayW, tangent2));
float dCore = length(p2d);
// Core (Neutron star) - Pulsating size
float pulse = 0.95 + 0.05 * sin(u_Params.x * u_Params.y * 8.0);
float coreSize = 0.058 * pulse;
float core = smoothstep(coreSize, 0.0, dCore);
// Detailed solar corona/flares on the halo
float flare = fbm(rayW * 12.0 + vec3(0.0, u_Params.x * 0.25, 0.0));
float halo = exp(-dCore * 16.0) * 3.2 + exp(-dCore * 3.5) * 0.9 * (0.7 + 0.3 * flare);
// 3. Wobbling vertical plasma jets (shakes back and forth)
float wobbleAngle = sin(u_Params.x * u_Params.y * 6.0) * 0.22;
vec2 jetDir2d = vec2(sin(wobbleAngle), cos(wobbleAngle));
float distAlong = dot(p2d, jetDir2d);
// Outward propagating plasma displacement waves
float wave = sin(abs(distAlong) * 75.0 - u_Params.x * u_Params.y * 32.0) * 0.0055 * abs(distAlong);
float distToJet = length(p2d - distAlong * jetDir2d) - wave;
// Collimated core jet + soft glowing outer shell, fading along the distance
float jetIntensity = (exp(-distToJet * 320.0) * 3.0 + exp(-distToJet * 38.0) * 0.8);
jetIntensity *= exp(-abs(distAlong) * 0.85);
// High frequency electrical flicker/sparkle of the jets
float jetFlicker = 0.82 + 0.18 * sin(u_Params.x * 115.0) * cos(u_Params.x * 145.0);
// 4. Combine Pulsar Layers
vec3 coreColor = vec3(1.0, 1.0, 1.0);
vec3 haloColor = vec3(0.18, 0.45, 1.0) * u_Color.rgb;
vec3 jetColor = vec3(0.35, 0.68, 1.0) * u_Color.rgb;
vec3 pulsarCol = mix(vec3(0.0), coreColor, core);
pulsarCol += haloColor * halo;
pulsarCol += jetColor * jetIntensity * jetFlicker;
// Apply front mask to ensure smooth fade at horizon/boundaries
col += pulsarCol * frontMask;
}
// Apply global intensity adjustment and alpha transparency
col *= (1.0 + u_Params.w * 10.0);
color = vec4(col, u_Color.a);
}

View file

@ -0,0 +1,211 @@
#version 330 core
layout (std140) uniform ShaderSky {
vec4 u_Color; // rgb = tint, a = alpha
vec4 u_Params; // x = time, y = speed, z = scale, w = intensity
vec4 u_View; // x/y = resolution, z = fov
vec4 u_Cam; // x = yaw, y = pitch
vec4 u_Thunder; // x = interval, y = chance, z = glow
};
out vec4 color;
mat3 rotX(float a) {
float c = cos(a), s = sin(a);
return mat3(1.0, 0.0, 0.0,
0.0, c, s,
0.0, -s, c);
}
mat3 rotY(float a) {
float c = cos(a), s = sin(a);
return mat3( c, 0.0, s,
0.0, 1.0, 0.0,
-s, 0.0, c);
}
// Highly optimized fast hash function using GPU hardware dot & sin
float hash(vec3 p) {
return fract(sin(dot(p, vec3(127.1, 311.7, 74.7))) * 43758.5453123);
}
float hash(float n) {
return fract(sin(n) * 43758.5453123);
}
// Vectorized 3D noise for maximum GPU throughput
float noise(vec3 p) {
vec3 i = floor(p);
vec3 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float n000 = hash(i + vec3(0.0, 0.0, 0.0));
float n100 = hash(i + vec3(1.0, 0.0, 0.0));
float n010 = hash(i + vec3(0.0, 1.0, 0.0));
float n110 = hash(i + vec3(1.0, 1.0, 0.0));
float n001 = hash(i + vec3(0.0, 0.0, 1.0));
float n101 = hash(i + vec3(1.0, 0.0, 1.0));
float n011 = hash(i + vec3(0.0, 1.0, 1.0));
float n111 = hash(i + vec3(1.0, 1.0, 1.0));
vec4 a = mix(vec4(n000, n010, n001, n011),
vec4(n100, n110, n101, n111), f.x);
vec2 b = mix(a.xz, a.yw, f.y);
return mix(b.x, b.y, f.z);
}
float fbm(vec3 p) {
float v = 0.0;
float a = 0.5;
vec3 shift = vec3(100.0);
for (int i = 0; i < 4; ++i) {
v += a * noise(p);
p = p * 2.0 + shift;
a *= 0.5;
}
return v;
}
void main() {
vec2 uv = gl_FragCoord.xy / u_View.xy;
vec2 sp = uv * 2.0 - 1.0;
float aspect = u_View.x / u_View.y;
float tanV = tan(radians(u_View.z) * 0.5);
vec3 rayV = normalize(vec3(sp.x * tanV * aspect, sp.y * tanV, 1.0));
vec3 rayW = rotY(u_Cam.x) * rotX(u_Cam.y) * rayV;
// 1. Storm clouds overhead
float cloudDensity = 0.0;
if (rayW.y > -0.15) {
// FBM 3D noise for cloud density, scrolled over time
vec3 p = rayW * u_Params.z;
p.x += u_Params.x * u_Params.y * 0.08;
p.z += u_Params.x * u_Params.y * 0.04;
cloudDensity = fbm(p);
// Fade clouds near and below horizon
cloudDensity *= smoothstep(-0.15, 0.3, rayW.y);
}
// 2. Procedural lightning generator
float interval = max(1.0, u_Thunder.x); // lightning triggers every interval seconds
float timeIndex = floor(u_Params.x / interval);
float timeOffset = fract(u_Params.x / interval);
// Decisions based on time index
float strikeHash = hash(timeIndex * 12.34);
bool hasStrike = strikeHash < u_Thunder.y; // Custom chance of strike
float flash = 0.0;
float boltGlow = 0.0;
if (hasStrike) {
// Random horizontal angle for the lightning bolt
float strikeAngle = hash(timeIndex * 45.67) * 6.28318;
// Define bolt start (high sky) and end (near horizon)
vec3 boltStart = normalize(vec3(cos(strikeAngle), 1.0, sin(strikeAngle)));
vec3 boltEnd = normalize(vec3(cos(strikeAngle + (hash(timeIndex * 8.3) - 0.5) * 0.2), -0.2, sin(strikeAngle + (hash(timeIndex * 8.3) - 0.5) * 0.2)));
float t = timeOffset;
if (t < 0.55) {
// Electrical flicker overlay (high frequency hum)
float flicker = 0.8 + 0.2 * sin(u_Params.x * 95.0) * cos(u_Params.x * 135.0);
// Realistic double-flash lighting pattern
flash = (exp(-t * 22.0) * 1.5 + exp(-abs(t - 0.15) * 25.0) * 1.0 + exp(-abs(t - 0.35) * 15.0) * 0.4) * flicker;
// Build the lightning bolt
float boltIntensity = max(exp(-t * 18.0), exp(-abs(t - 0.15) * 22.0));
if (boltIntensity > 0.02) {
vec3 segment = boltEnd - boltStart;
float segLen = length(segment);
vec3 segDir = segment / segLen;
// Project ray onto the segment
float h = dot(rayW - boltStart, segDir);
h = clamp(h, 0.0, segLen);
vec3 projection = boltStart + segDir * h;
// Generate detailed jagged offsets using multiple octaves of sine waves
float noiseSeed = timeIndex * 73.19;
float jagged = sin(h * 30.0 + noiseSeed) * 0.035;
jagged += cos(h * 70.0 - noiseSeed) * 0.015;
jagged += sin(h * 150.0 + noiseSeed * 1.5) * 0.007;
jagged += cos(h * 350.0 - noiseSeed * 3.0) * 0.003; // extra high frequency details
vec3 offsetProj = projection + vec3(jagged, 0.0, jagged * 0.6);
// Distance from camera ray to the jagged bolt
float distToBolt = length(rayW - normalize(offsetProj));
// Hot white core + soft blue/purple glow
boltGlow = (exp(-distToBolt * 280.0) * 2.5 + exp(-distToBolt * 30.0) * 0.6) * boltIntensity * flicker;
// Branch 1 (going right)
if (hash(timeIndex * 29.5) > 0.3 && h > segLen * 0.2) {
vec3 bStart = projection;
vec3 bEnd = normalize(boltEnd + vec3(sin(strikeAngle + 0.8), -0.15, cos(strikeAngle + 0.8)) * 0.4);
vec3 bSeg = bEnd - bStart;
float bLen = length(bSeg);
if (bLen > 0.0) {
vec3 bDir = bSeg / bLen;
float bh = dot(rayW - bStart, bDir);
bh = clamp(bh, 0.0, bLen);
vec3 bProj = bStart + bDir * bh;
float bJagged = sin(bh * 45.0 + noiseSeed) * 0.015 + cos(bh * 110.0) * 0.006;
vec3 bOffsetProj = bProj + vec3(bJagged, 0.0, bJagged);
float distToBranch = length(rayW - normalize(bOffsetProj));
boltGlow += (exp(-distToBranch * 220.0) * 1.2 + exp(-distToBranch * 30.0) * 0.3) * boltIntensity * (1.0 - bh/bLen) * flicker;
}
}
// Branch 2 (going left, from a different position)
if (hash(timeIndex * 83.2) > 0.5 && h > segLen * 0.4) {
vec3 bStart = projection;
vec3 bEnd = normalize(boltEnd + vec3(sin(strikeAngle - 0.8), -0.2, cos(strikeAngle - 0.8)) * 0.45);
vec3 bSeg = bEnd - bStart;
float bLen = length(bSeg);
if (bLen > 0.0) {
vec3 bDir = bSeg / bLen;
float bh = dot(rayW - bStart, bDir);
bh = clamp(bh, 0.0, bLen);
vec3 bProj = bStart + bDir * bh;
float bJagged = sin(bh * 50.0 - noiseSeed) * 0.015 + cos(bh * 120.0) * 0.006;
vec3 bOffsetProj = bProj + vec3(bJagged, 0.0, bJagged);
float distToBranch = length(rayW - normalize(bOffsetProj));
boltGlow += (exp(-distToBranch * 220.0) * 1.2 + exp(-distToBranch * 30.0) * 0.3) * boltIntensity * (1.0 - bh/bLen) * flicker;
}
}
}
}
}
// 3. Compose Colors
// Theme color input is blended with a dark storm color scheme
vec3 stormBase = mix(vec3(0.005, 0.005, 0.015), u_Color.rgb * 0.05, 0.5);
// Background sky flashes scaled by custom glow multiplier
vec3 flashColor = vec3(0.75, 0.82, 1.0) * flash * u_Thunder.z;
vec3 finalSky = stormBase + flashColor * 0.25;
// Ambient cloud lighting from within scaled by custom glow multiplier
vec3 cloudBaseColor = mix(vec3(0.01, 0.01, 0.02), vec3(0.06, 0.07, 0.12), cloudDensity);
vec3 cloudLitColor = mix(vec3(0.02, 0.025, 0.05), vec3(0.8, 0.85, 1.0), cloudDensity * flash * u_Thunder.z * 0.7);
vec3 finalCloud = mix(cloudBaseColor, cloudLitColor, min(flash * u_Thunder.z, 1.0));
// Blend background sky and storm clouds
float cloudMix = smoothstep(0.18, 0.48, cloudDensity);
vec3 finalColor = mix(finalSky, finalCloud, cloudMix);
// Layer the bright lightning bolt on top scaled by custom glow multiplier
finalColor += vec3(0.85, 0.92, 1.0) * boltGlow * u_Thunder.z;
// Apply global intensity adjustment and alpha transparency
finalColor *= (1.0 + u_Params.w * 10.0);
color = vec4(finalColor, u_Color.a);
}