feat: реорганизация combat/visuals модулей + добавлен InvisESP
This commit is contained in:
parent
ff42902c43
commit
4a529466c7
27 changed files with 1478 additions and 2602 deletions
564
combatant-client-26.2/DEV_GUIDE.md
Normal file
564
combatant-client-26.2/DEV_GUIDE.md
Normal file
|
|
@ -0,0 +1,564 @@
|
|||
# LoVisual Mod — Dev Guide / Справочник по созданию модулей
|
||||
|
||||
Цель: быстро посмотреть структуру и написать новый модуль / HUD-элемент /
|
||||
команду, не перекапывая весь код. Всё ниже — сводка по реальному коду.
|
||||
|
||||
- Пакеты: `dev.loki.lovisual.*`
|
||||
- Minecraft 26.2, Fabric, Java 25, Lombok
|
||||
- Правила проекта (важно!): файл ≤200 строк, папка ≤3 файлов,
|
||||
`./gradlew build` после каждого этапа, тесты для новой логики,
|
||||
никаких `static final Minecraft` в полях — только ленивый `Minecraft.getInstance()`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Архитектура (главные пакеты)
|
||||
|
||||
```
|
||||
dev.loki.lovisual
|
||||
├── LoVisual.java — ClientModInitializer: весь старт (тики, рендер-фазы)
|
||||
├── Lifecycle.java — INIT → ACTIVE → SHUTDOWN; гейты isActive/canRunRender/canRunHud
|
||||
├── features/
|
||||
│ ├── module/ — СИСТЕМА МОДУЛЕЙ (главное для вас)
|
||||
│ │ ├── Module.java — базовый класс (1181 стр, фасад)
|
||||
│ │ ├── ModuleInfo — @annotation для модуля
|
||||
│ │ ├── ModuleManager — реестр + диспетчер фаз
|
||||
│ │ ├── ModuleAutoLoader — авто-обнаружение по аннотации (ClassGraph)
|
||||
│ │ ├── Modules — удобный доступ: Modules.get(Класс.class)
|
||||
│ │ ├── ModuleCategory — COMBAT/MOVEMENT/PLAYER/VISUALS/MISC
|
||||
│ │ ├── HudPhase / WorldPhase — фазы рендера
|
||||
│ │ └── modules/{combat,player,visuals,misc}/
|
||||
│ ├── command/ — команды: @CommandInfo + ClientCommand
|
||||
│ ├── gui/
|
||||
│ │ ├── clickgui/ — экран настроек (Setting → виджет)
|
||||
│ │ └── hud/ — HUD-элементы
|
||||
│ │ ├── draggable/ — перетаскиваемые (Fps, Coords, ModuleList, TargetHud…)
|
||||
│ │ └── nondraggable/ — статичные (CustomBar, CustomHotbar, BetterChat…)
|
||||
│ ├── relations/ — друзья/враги/категории (CategoryService)
|
||||
│ ├── security/ — BackdoorProtection (SSRF/translate-защита)
|
||||
│ └── theme/ — темы: Theme.theme() → Themes.Theme (цвета)
|
||||
├── config/
|
||||
│ ├── values/ — типы настроек: ConfigValue (bool/num/mode/color…)
|
||||
│ ├── SettingDef.java — GUI-нейтральный дескриптор настройки
|
||||
│ └── common/CommonSettingSchemas — переиспользуемые схемы (i18n-ключи)
|
||||
├── events/
|
||||
│ ├── EventBus (Events.BUS) — шина событий
|
||||
│ ├── @EventHandler — аннотация хендлера
|
||||
│ └── impl/ — все события (см. §5)
|
||||
├── mixins/ — миксины + accessors (mixins/accessors/)
|
||||
├── addon/ — система аддонов (API v0)
|
||||
├── api/v0/ — публичный API аддонов (module/client/render/clickgui)
|
||||
├── render/
|
||||
│ ├── engine/
|
||||
│ │ ├── renderer/Renderer2D — весь 2D-рендер (прямоугольники, круги, текст, items)
|
||||
│ │ ├── renderer/Renderer3D — 3D (линии, меши, quads)
|
||||
│ │ ├── text/ — Fonts.renderer("Onest", Regular), TextRenderer
|
||||
│ │ ├── animation/AnimationUtility
|
||||
│ │ ├── pipeline/LoVisualRenderPipelines — готовые пайплайны (UI_COLORED и т.д.)
|
||||
│ │ └── uniform/MeshBuilder — построение мешей
|
||||
│ └── engine/color/RenderColor — ARGB
|
||||
└── util/ — куча хелперов (см. §8)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Жизненный цикл и старт
|
||||
|
||||
В `LoVisual.onInitializeClient()` по порядку:
|
||||
|
||||
1. `LoVisualRenderEngineBootstrap.init()`
|
||||
2. `MainConfig.get()`, `AccountConfig.get()`, `CommandManager.init()`
|
||||
3. `ModuleAutoLoader.load("dev.loki.lovisual.features.module.modules")` — сканирует пакет,
|
||||
ищет классы с `@ModuleInfo`, создаёт через no-args конструктор и зовёт `module.postInit()`.
|
||||
4. `ModuleManager.loadAllModuleConfigs()` — применяет сохранённые конфиги, включает enabled-модули.
|
||||
5. `HudElements.init()`, `StaticHudElementBootstrap.init()` — HUD-элементы.
|
||||
6. `Events.BUS.register(...)` — реестрируются глобальные сервисы.
|
||||
7. `Lifecycle.activate()` — после этого события/тики работают.
|
||||
|
||||
**Вывод:** чтобы добавить модуль — просто положи класс с `@ModuleInfo` в пакет
|
||||
`...modules.<категория>` и всё. Реестрировать руками не нужно.
|
||||
|
||||
`Lifecycle` — главный гейт: `Lifecycle.isActive()`, `canRunRender()`, `canRunHud()`.
|
||||
До `activate()` модули и события не работают.
|
||||
|
||||
---
|
||||
|
||||
## 3. Модуль — быстрый старт
|
||||
|
||||
Минимальный модуль (по образцу `FullBright`):
|
||||
|
||||
```java
|
||||
package dev.loki.lovisual.features.module.modules.misc;
|
||||
|
||||
import dev.loki.lovisual.config.values.NumberValue;
|
||||
import dev.loki.lovisual.events.EventHandler;
|
||||
import dev.loki.lovisual.events.impl.GameTickEvent;
|
||||
import dev.loki.lovisual.features.module.Module;
|
||||
import dev.loki.lovisual.features.module.ModuleCategory;
|
||||
import dev.loki.lovisual.features.module.ModuleInfo;
|
||||
|
||||
@ModuleInfo(
|
||||
id = "my_feature",
|
||||
displayName = "My Feature",
|
||||
aliases = {"mf"},
|
||||
category = ModuleCategory.MISC,
|
||||
enabledByDefault = false
|
||||
)
|
||||
public class MyFeature extends Module {
|
||||
|
||||
private final NumberValue<Integer> amount =
|
||||
num("myFeatureAmount", 3, 0, 20); // имя_конфига, дефолт, мин, макс
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
// вызывается при включении
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTick() {
|
||||
// каждый игровой тик, если модуль включён
|
||||
if (!isEnabled()) return;
|
||||
int a = amount.get();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `@ModuleInfo`
|
||||
```java
|
||||
id = "имя" // нижний регистр, уникальный, это ключ конфига и команды
|
||||
displayName = "Имя" // показывается в UI; можно i18n-ключ
|
||||
aliases = {} // необязательно
|
||||
category = ModuleCategory.XXX
|
||||
enabledByDefault = false
|
||||
description = "" // или i18n-ключ "module.<id>.description"
|
||||
```
|
||||
|
||||
### Автогенерируемые настройки модуля
|
||||
У каждого модуля уже есть (создаются в конструкторе `Module`):
|
||||
- `enabled` (BooleanValue) — состояние,
|
||||
- `bind` (KeyBindSetting, значение `KeyBindValue`) — клавиша,
|
||||
- `activation_source`,
|
||||
- `show_in_module_list`.
|
||||
|
||||
`postInit()` вызывается автоматически лоадером: читает поля-настройки, делает
|
||||
`SettingFactory.fromDefs(...)`, регистрирует модуль в `ModuleManager`.
|
||||
|
||||
### Хуки жизненного цикла (переопределяемые)
|
||||
| Метод | Когда |
|
||||
|-------|-------|
|
||||
| `onEnable()` / `onDisable()` | переключение модуля |
|
||||
| `onTick()` | каждый игровой тик (в мире) |
|
||||
| `onFrame(float tickDelta)` | каждый кадр |
|
||||
| `onKey(int key, int action)` | нажатия клавиш |
|
||||
| `onRender2D(GuiGraphicsExtractor)` | vanilla HUD pass |
|
||||
| `onRenderHudEngine(Renderer2D, TextRenderer, ...)` | кастомный 2D-рендер (см. §6) |
|
||||
| `onRenderHudEngineForeground(...)` | 2D поверх всего |
|
||||
| `onRenderWorld(PoseStack, SubmitNodeCollector)` | 3D vanilla-style |
|
||||
| `onRenderWorldEngine(Renderer3D, Renderer3D, float)` | 3D через движок (см. §7) |
|
||||
| `getHudPhase()` / `getWorldPhase()` | фаза, в которой рендерить |
|
||||
|
||||
> `getHudPhase()`/`getWorldPhase()` по умолчанию `NONE` — модуль вообще не рендерится
|
||||
> диспетчером. Верните фазу, чтобы рендер-хуки вызывались (см. §6/§7).
|
||||
|
||||
### Управление состоянием
|
||||
- `isEnabled()`, `setEnabled(boolean)`, `setEnabled(boolean, ModuleActivationSource)`, `toggle()`
|
||||
- `isAvailable()` / `getAvailabilityReason()` — переопределите `getUnavailableReason()`
|
||||
- `name()`, `getDisplayName()`, `getAliases()`, `getCategory()`, `getDescription()`
|
||||
- `saveConfig()`, `getConfigValue(String)`, `getSettings()`
|
||||
|
||||
### Доступ к другим модулям
|
||||
```java
|
||||
import dev.loki.lovisual.features.module.Modules;
|
||||
|
||||
Freecam fc = Modules.get(Freecam.class); // null если не активен/не зарегистрирован
|
||||
boolean on = Modules.enabled("freecam"); // по id
|
||||
```
|
||||
`ModuleManager.get(String id)` / `ModuleManager.get(Class)` / `ModuleManager.require(Class)` тоже доступны.
|
||||
|
||||
---
|
||||
|
||||
## 4. Настройки (ConfigValue → Setting)
|
||||
|
||||
Базовый класс: `config/values/ConfigValue<T>` — `get()`, `set(v)`, `getName()`,
|
||||
`toJson()`, `fromJson(Object)`, `toDisplay()`.
|
||||
|
||||
Внутри модуля настройки создаются **хелперами** в `Module` (все `protected final`).
|
||||
Имя в конфиге и `settingId` (i18n) могут различаться.
|
||||
|
||||
### Хелперы в Module / BaseHudElement
|
||||
| Хелпер | Тип | Пример |
|
||||
|--------|-----|--------|
|
||||
| `bool(name, def)` | `BooleanValue` | `bool("my_on", true)` |
|
||||
| `num(name, def, min, max)` | `NumberValue<Integer/Long/Float/Double>` | `num("my_num", 3, 0, 20)` |
|
||||
| `mode(name, def, options...)` | `ModeValue` (String) | `mode("my_mode", "A", "A", "B", "C")` |
|
||||
| `enumMode(name, defEnum)` / `enumSetting(name, defEnum, ...)` | `EnumValue<E>` | см. `Reach` |
|
||||
| `color(name, "#AARRGGBB")` | `RGBAColorValue` | `color("my_bg", "#F7343434")` |
|
||||
| `colorNoAlpha(name, "#RRGGBB")` | `RGBColorValue` | `colorNoAlpha("my_fg", "#FFFFFF")` |
|
||||
| `text(name, def)` | `StringValue` | `text("my_text", "")` |
|
||||
| `textList(name)` / `textList(name, pickerMode)` | `SetValue` (множество строк) | `textList("my_list")` |
|
||||
| `itemList(name)` | `ItemIdSetValue` (список предметов) | `itemList("my_items")` |
|
||||
| `group(name, defaultsMap)` | `BooleanMapValue` (группа чекбоксов) | `group("gui_sounds", Map.of(...))` |
|
||||
| `bind(name, def, mode)` | `KeyBindValue` | `bind("my_bind", "R", BindMode.PRESS)` |
|
||||
| `action(name, defaultKey, mode)` | `FunctionBindSetting` | см. ниже |
|
||||
| `setDefaultBind("KEY")` | — | клавиша модуля (см. `ClickGui`) |
|
||||
|
||||
**Варианты с общим i18n** (`*Common`): `boolCommon`, `numCommon`, `modeCommon`,
|
||||
`enumCommon`, `color` (без common) — берут схему из `CommonSettingSchemas`:
|
||||
|
||||
```java
|
||||
distance = numCommon("reachDistance", "range", CommonSettingSchemas.COMBAT_RANGE, 4.5, 3.0, 6.0);
|
||||
mode = enumCommon("reachMode", "mode", CommonSettingSchemas.ANTICHEAT_MODE, Mode.NORMAL, Mode.values());
|
||||
```
|
||||
|
||||
### Условная видимость / доступность
|
||||
```java
|
||||
private final NumberValue<Double> wallDistance =
|
||||
visibleWhen(numCommon("reachWallDistance", "wall_range", schema, 3.0, 0.0, 6.0),
|
||||
() -> raycast.get() == RaycastMode.THROUGH_WALLS);
|
||||
|
||||
// недоступен (показывается серым + причина):
|
||||
appliesWhen(mySetting, () -> mc.player != null, "Только в мире");
|
||||
```
|
||||
|
||||
### Экшены (вторая кнопка на модуль)
|
||||
```java
|
||||
action("climb", "G", BindMode.PRESS); // создаёт FunctionBindSetting
|
||||
|
||||
// в тике:
|
||||
if (isActionPressedOnce("climb")) { ... } // фронт нажатия
|
||||
if (isActionHeld("climb")) { ... } // удержание
|
||||
```
|
||||
|
||||
### Настройки в HUD-элементах
|
||||
У `BaseHudElement` те же хелперы (`bool`, `num`, `mode`, `enumSetting`, `color`,
|
||||
`textList`, `bind`, `group`, `visibleWhen`), но объявление идёт в
|
||||
`defineSettings(List<SettingDef>)` (см. §9).
|
||||
|
||||
### Виджеты в UI (SettingDef)
|
||||
`SettingDef` — GUI-нейтральный дескриптор. `SettingFactory.fromDef(def)` превращает
|
||||
его в виджет. Доступные `Kind`: `BOOLEAN, NUMBER, MODE, COLOR, COLOR_NO_ALPHA,
|
||||
TEXT, TEXT_LIST, COOLDOWN_RULES, PROTOCOL_HEURISTICS, GROUP, BIND`.
|
||||
Для модулей SettingDef строятся автоматически из хелперов — руками не нужно.
|
||||
`@DisableSettingI18n(name = false, options = true)` — отключить i18n у настройки.
|
||||
|
||||
---
|
||||
|
||||
## 5. События (EventBus)
|
||||
|
||||
`Events.BUS.post(event)` — доставка хендлерам. Хендлер — любой метод с одним
|
||||
аргументом-наследником `Event`, помеченный `@EventHandler(priority = N)`.
|
||||
Подписчики регистрируются автоматически при `ModuleManager.register()` (модули),
|
||||
или явно `Events.BUS.register(obj)` (в `LoVisual.onInitializeClient`).
|
||||
|
||||
Модули-подписчики автоматически гейтятся: если модуль выключен — его хендлеры не вызываются.
|
||||
Поэтому в хендлерах модуля проверка `if (!isEnabled()) return;` не обязательна, но
|
||||
встречается в коде для ясности.
|
||||
|
||||
`Event` — база с `cancelled`/`cancel()`. Наследование событий поддерживается.
|
||||
|
||||
Полный список `events/impl/` (по имени файла):
|
||||
|
||||
```
|
||||
AttackEntityEvent BlinkPacketEvent CombatProtocolBossbarEvent
|
||||
CrosshairTargetUpdateEvent EventBreakBlock EventCollision EventPostSync
|
||||
EventPushOutOfBlocks EventSync EventTargetChanged FireworkEvent GameTickEvent
|
||||
I18nPreflightCollectEvent KeybindIsPressedEvent KeyInputEvent LightmapEvent
|
||||
LightmapModifyEvent MovementInputEvent PacketEvent PlayerJumpEvent PlayerMoveEvent
|
||||
PlayerSafeWalkEvent PlayerStepEvent PlayerStepSuccessEvent PlayerVelocityStrafe
|
||||
PostPlayerUpdateEvent PvpChatEvent PvpOverlayEvent PvpTabEvent
|
||||
RenderPrewarmCollectEvent RotationUpdateEvent SprintControlEvent
|
||||
```
|
||||
|
||||
Типичные события для нового модуля:
|
||||
- `GameTickEvent` — каждый тик,
|
||||
- `PacketEvent` — сетевые пакеты,
|
||||
- `AttackEntityEvent`, `EventTargetChanged` — бой/цели,
|
||||
- `LightmapModifyEvent` — свет (пример `FullBright`),
|
||||
- `PlayerMoveEvent`, `MovementInputEvent` — движение,
|
||||
- `RenderPrewarmCollectEvent` — «прогреть» шрифты/текстуры для вашего рендера.
|
||||
|
||||
Пример:
|
||||
```java
|
||||
@EventHandler
|
||||
private void onTick(GameTickEvent event) { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 2D-рендер (HUD)
|
||||
|
||||
### Фазы
|
||||
Верните не-`NONE` фазу из `getHudPhase()`:
|
||||
```java
|
||||
HudPhase: FIRST, BEFORE_MISC_OVERLAYS, AFTER_MISC_OVERLAYS, AFTER_BOSS_BAR,
|
||||
BEFORE_DEMO_TIMER, BEFORE_CHAT, AFTER_SUBTITLES, LAST
|
||||
```
|
||||
`ModuleManager.renderHudEngine(phase, renderer, textRenderer, ctx, tickDelta)` вызывает
|
||||
у модуля `onRenderHudEngine(...)`; `renderHudEngineForeground` → `onRenderHudEngineForeground`.
|
||||
|
||||
### Renderer2D — главный 2D API
|
||||
Включается/выключается через `begin()` / `render()` (уже делает диспетчер).
|
||||
Готовые методы (фасад `Renderer2D.java`, все в `px`):
|
||||
|
||||
- **Фигуры:** `quad(x,y,w,h,argb)`, `quad(x,y,w,h,cTL,cTR,cBR,cBL)`,
|
||||
`roundedRect(x,y,w,h,radius,argb)`, `roundedRectStroke(...)`,
|
||||
`roundedRectGradient(...)`, `roundedRectGlow/Shadow/SoftShadow(...)`,
|
||||
`circle(cx,cy,r,argb)`, `circleStroke(...)`, `arcStroke(...)`,
|
||||
`line(x1,y1,x2,y2,argb)`, `boxLines(...)`, `roundedSmokeFill(...)`.
|
||||
- **Текст:** см. §6.2.
|
||||
- **Предметы:** `item(ItemStack, x, y, scale)`, `itemUnscaled(...)`, `itemPivot(...)`.
|
||||
- **SVG/текстуры:** `svg(Identifier/Path, x, y, w, h)`, `textureQuad(...)`, `msdfTextureQuad(...)`.
|
||||
- **Effect (стеклянные панели):** `effect(UiEffectSpec)`.
|
||||
|
||||
### Текст
|
||||
```java
|
||||
TextRenderer tr = Fonts.renderer("Onest", FontInfo.Type.Regular, textRenderer);
|
||||
// или Fonts.renderer("OnestMedium", Regular), "OnestBold", "Inter", "Iosevka", "Icons"...
|
||||
tr.begin(scale, true, false);
|
||||
double w = tr.getWidth("text", false);
|
||||
double h = tr.getHeight(false);
|
||||
tr.render("text", x, y, new RenderColor(0xFFFFFFFF), false);
|
||||
tr.end();
|
||||
```
|
||||
Универсальная запись «в одном блоке»:
|
||||
```java
|
||||
textRenderer.begin(scale, true, false);
|
||||
textRenderer.render("FPS", x, y, new RenderColor(argb), false);
|
||||
textRenderer.end();
|
||||
```
|
||||
`TextRenderer.get()` — текущий рендерер (передаётся в хук). `HudScale.scale(screenW, screenH)`
|
||||
даёт масштаб для адаптивного HUD (см. `Fps`).
|
||||
|
||||
### Иконки / текстуры
|
||||
Идентификаторы: `Identifier.fromNamespaceAndPath("lovisual", "textures/hud/elements/fps.png")`.
|
||||
SVG лежат в `assets/lovisual/svg/`.
|
||||
|
||||
### Анимации
|
||||
`AnimationUtility`: `approach(value, target, dt, speed)`, `fast(...)`, `lerp(...)`,
|
||||
`easeOutCubic`, `easeInOutCubic`, `easeOutBack`, `blink(ms)`, `deltaTime()`, `snap(...)`.
|
||||
|
||||
### Хелперы
|
||||
- `HudRenderUtil`: `setAlpha(argb, a)`, `mixColor(a, b, t)`, `glassBackground()`,
|
||||
`drawLiquidGlass(...)`, `animateVisibility(...)`, `visibilityScale(...)`.
|
||||
- `HudGlobalConfig.get()`: `getFontSize()`, `getBlurRadius()` и т.д.
|
||||
- Тема: `Theme.theme()` (статический импорт `dev.loki.lovisual.features.theme.Theme.theme`)
|
||||
→ `Themes.Theme` c полями `windowBg, windowHeader, windowStroke, surface, surfaceHover,
|
||||
cardEnabled, cardDisabled, textPrimary, textMuted, accent, accentSoft, strokeSoft`
|
||||
(всё `int` ARGB). См. `Fps.updatePalette()`.
|
||||
|
||||
---
|
||||
|
||||
## 7. 3D-рендер (мир)
|
||||
|
||||
### Фазы
|
||||
Верните не-`NONE` фазу из `getWorldPhase()`:
|
||||
```java
|
||||
WorldPhase: BEFORE_ENTITIES, AFTER_ENTITIES, BEFORE_TRANSLUCENT, END_MAIN, AFTER_POST_PROCESS
|
||||
```
|
||||
|
||||
### Renderer3D — простой путь
|
||||
В хуке `onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta)`:
|
||||
|
||||
```java
|
||||
renderer.begin();
|
||||
renderer.line(x1, y1, z1, x2, y2, z2, r, g, b, a); // линия
|
||||
renderer.quad(x1,y1,z1, x2,y2,z2, x3,y3,z3, x4,y4,z4); // квад
|
||||
renderer.triangle(...);
|
||||
renderer.render(new PoseStack()); // в конце
|
||||
```
|
||||
Пример: `Tracers.onRenderWorldEngine` (линии от камеры к игрокам).
|
||||
|
||||
### MeshBuilder + RenderPipeline — продвинутый путь
|
||||
```java
|
||||
MeshBuilder mesh = new MeshBuilder(LoVisualRenderPipelines.UI_TEXTURED_ADDITIVE);
|
||||
mesh.begin();
|
||||
mesh.ensureQuadCapacity();
|
||||
int i1 = mesh.vec2(x, y).vec2(u, v).color(r, g, b, a).next();
|
||||
int i2 = mesh.vec2(...).vec2(...).color(...).next();
|
||||
mesh.quad(i1, i2, i3, i4);
|
||||
mesh.end();
|
||||
```
|
||||
Готовые пайплайны в `LoVisualRenderPipelines` (фрагмент):
|
||||
```
|
||||
UI_COLORED, UI_COLORED_LINES, UI_TEXTURED, UI_TEXTURED_ADDITIVE, UI_TEXT, UI_TEXT_MSDF,
|
||||
UI_SVG_MSDF, UI_BLUR, WORLD_COLORED, WORLD_COLORED_LINES, WORLD_COLORED_DEPTH,
|
||||
WORLD_TEXTURED, WORLD_TEXTURED_DEPTH, WORLD_TEXT, WORLD_TEXT_MSDF, WORLD_DECAL_SDF,
|
||||
WORLD_COLORED_LINES_DEPTH, WORLD_WIDE_COLORED_LINES, ...HAND_*, SHADER_ESP_*, POST_FX...
|
||||
```
|
||||
Отправить на отрисовку (2D-стрелки из `Tracers`):
|
||||
```java
|
||||
MeshRenderer.begin()
|
||||
.attachments(mc.gameRenderer.mainRenderTarget())
|
||||
.pipeline(LoVisualRenderPipelines.UI_TEXTURED_ADDITIVE)
|
||||
.mesh(mesh).transform(matrix4f)
|
||||
.sampler("u_Texture", view, sampler)
|
||||
.end();
|
||||
```
|
||||
`Renderer3D.Cull` — утилиты отсечения: `isInFrustum(...)`, `isSectionVisible(...)`, `isInFront(...)`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Полезные утилиты (`util/`)
|
||||
|
||||
| Пакет / класс | Что даёт |
|
||||
|---------------|----------|
|
||||
| `util.target.TargetManager` | текущая цель: `getTarget()`, `getTarget(true/false)`, `onAttack()`, `setPredictionTarget()`, `Source` |
|
||||
| `util.pvp.PvpTracker`, `PvpTargetTracker` | статистика PvP / отслеживание цели |
|
||||
| `util.aiming.RotationManager` | плавные/снап-ротации |
|
||||
| `util.combat.VulcanReachController` | клампинг дистанции (режим античита) |
|
||||
| `util.time.GameClock` | `millis()` — игровое время (тики×50мс), вне мира — стенное |
|
||||
| `util.time.TimerController` | таймеры |
|
||||
| `util.input.KeyManager` | `wasPressed("func")`, `isHeld("func")`, `isComboHeldAllowScreen(...)` |
|
||||
| `util.screen.ClientScreen` | текущий экран: `ClientScreen.current(mc)` / `.show(mc, screen)` |
|
||||
| `util.text` | текст-хелперы |
|
||||
| `util.entity.simulation.PlayerSimulationCache`, `BoatSimulationCache` | предсказание позиций |
|
||||
| `util.raycast` | рейкасты |
|
||||
| `util.item.OmniItemUtils` | утилиты предметов |
|
||||
| `util.combat.protocol.CombatProtocolHeuristics` | эвристики античита |
|
||||
| `util.logging.DebugLog` | `DebugLog.info/warn/error/config(...)` (printf-style) |
|
||||
| `util.resources.RenderResourceReadiness` | готовность рендер-ресурсов |
|
||||
| `features.relations.CategoryService` | `isFriend(Player)`, `getColor(Player)` — категории игроков |
|
||||
| `util.FastFps` | `getFps()` — быстрый FPS без накладок |
|
||||
|
||||
Ленивый доступ к MC:
|
||||
```java
|
||||
private final Minecraft mc = Minecraft.getInstance();
|
||||
```
|
||||
(поле, но это ок — поле инициализируется при создании модуля, а модули создаются
|
||||
в рантайме; правило «никакого `static final Minecraft` в полях»).
|
||||
|
||||
---
|
||||
|
||||
## 9. HUD-элементы
|
||||
|
||||
### Draggable (перетаскиваемый) — пример `Fps`
|
||||
```java
|
||||
@HudElementInfo(id = "fps", displayName = "FPS", enabledByDefault = true, order = 120)
|
||||
public final class Fps extends DraggableHudElement {
|
||||
// настройки — прямо поля:
|
||||
private final NumberValue<Double> scale = num("fps_scale", 2.37, 0.5, 5.0);
|
||||
private final RGBColorValue iconColor = visibleWhen(colorNoAlpha("fps_icon_color", "#FFFFFF"), this::isCustomMode);
|
||||
|
||||
@Override
|
||||
public void applyDefaultPosition(int screenW, int screenH) { this.x = 16f; this.y = 20f; }
|
||||
|
||||
@Override
|
||||
public boolean usesEngineRenderer() { return true; }
|
||||
|
||||
@Override
|
||||
public void renderEngine(Renderer2D renderer, TextRenderer textRenderer,
|
||||
GuiGraphicsExtractor ctx, float tickDelta,
|
||||
int screenW, int screenH) {
|
||||
if (!preview && !isEnabled()) { width = 0f; height = 0f; return; }
|
||||
// считаем width/height, рисуем через renderer + Fonts.renderer(...)
|
||||
}
|
||||
}
|
||||
```
|
||||
- Настройки объявляются как поля с хелперами (автоматом попадают в SettingDef).
|
||||
- `defaultLayout(x, y, anchorX, anchorY)` / `defaultLinkedLayout(...)` — стартовое положение.
|
||||
- `x, y, width, height` — позиция и размер (обновляйте width/height в renderEngine!).
|
||||
- Регистрация автоматическая (сканирование по `@HudElementInfo`).
|
||||
- Интерактив: `isMouseOverInteractive(mx, my)`, `onMouseClicked(mx, my, button)`.
|
||||
|
||||
### Nondraggable (статичный)
|
||||
Классы в `features/gui/hud/nondraggable/impl/` (CustomBar, CustomHealthBar, CustomHotbar,
|
||||
BetterTooltips, BetterButtons, DynamicIsland). Наследуют `BaseHudElement`, рендер через
|
||||
`renderEngine(...)` + `getRenderSpace()`/`getHudPhase()`.
|
||||
|
||||
---
|
||||
|
||||
## 10. Команды
|
||||
|
||||
Интерфейс `ClientCommand` + аннотация `@CommandInfo`, регистрация — вручную в `CommandManager`.
|
||||
|
||||
```java
|
||||
@CommandInfo(
|
||||
id = "toggle",
|
||||
aliases = "t",
|
||||
usage = "@toggle <module> [on|off|toggle]",
|
||||
descriptionKey = "command.toggle.description"
|
||||
)
|
||||
public final class ToggleCommand implements ClientCommand {
|
||||
@Override
|
||||
public boolean execute(CommandContext ctx) {
|
||||
// ctx.arg(0), ctx.arg(1), ...
|
||||
CommandOutput.success("Done");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> suggest(CommandContext ctx, int argIndex, String token) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
```
|
||||
- `CommandContext(mc, raw, name, args)` — `arg(i)` возвращает null если нет.
|
||||
- `CommandOutput` — `success/warning/error/send(String)` с префиксом `[LoVisual]`.
|
||||
- `CommandManager.init()` регистрирует команды из `impl/` (через ClassGraph, как модули).
|
||||
|
||||
---
|
||||
|
||||
## 11. API для аддонов (`api/v0/`)
|
||||
|
||||
- `LoVisualAddon` — entrypoint `"lovisual:addon"`: `onConfigureModules`,
|
||||
`onInitialize(LoVisualAddonContext)`, `onClientReady`, `onShutdown`.
|
||||
- `LoVisualModuleExtension` — расширение модуля (`ModuleExtensionContext`),
|
||||
перехватывает `beforeEnable/afterEnable/beforeTick/...` (см. `ModuleExtensionManager`).
|
||||
- `LoVisualClientApi.get()` — `modules()`, `module(id)`, `isModuleEnabled(id)`, `setModuleEnabled`.
|
||||
- `render/` — `LoVisualRenderCallback` + `LoVisualRenderStage`
|
||||
(HUD_RAW, HUD_SCALED, HUD_LOGICAL, WORLD_*, SCREEN_*), `LoVisualPostProcessCallback`,
|
||||
`LoVisualUniforms`, `LoVisualRenderPipelineBuilder`.
|
||||
- `clickgui/` — `LoVisualClickGuiSection`, `LoVisualClickGuiRenderContext` — свои секции в кликгую.
|
||||
|
||||
---
|
||||
|
||||
## 12. i18n (языковые ключи)
|
||||
|
||||
Файлы: `src/main/resources/assets/lovisual/lang/en_us.json`, `ru_ru.json`.
|
||||
|
||||
Соглашения по ключам:
|
||||
- Модуль: `module.<id>`, `module.<id>.name`, `module.<id>.description`,
|
||||
настройка: `module.<id>.setting.<setting_id>`, опции: `module.<id>.option.<option>`.
|
||||
- HUD draggable: `hud.draggable.<id>` / `setting.draggable.<id>.<setting>`.
|
||||
- HUD static: `hud.static.<id>`.
|
||||
- Команды: `command.<id>.description`.
|
||||
- Общие схемы: `CommonSettingSchemas` указывают `commonI18nKeys` (например `combat.range`)
|
||||
— они ищутся в lang-файлах с префиксом `setting.common.<key>` (проверьте по `CommonKey`).
|
||||
|
||||
`I18n.get("key")` — получение перевода. Если ключа нет — возвращается сам ключ.
|
||||
В `Module.getDisplayName()`/`getDescription()` fallback на `displayName`/`description`.
|
||||
|
||||
---
|
||||
|
||||
## 13. Миксины и accessors
|
||||
|
||||
- Все миксины в `mixins/` (`@Mixin(...)`). Пакеты: `accessors`, `iris`, `sodium`, `moreculling`,
|
||||
`xaero`, `security`.
|
||||
- Accessors в `mixins/accessors/` — примеры: `MinecraftAccessor`, `LocalPlayerAccessor`,
|
||||
`PlayerInventoryAccessor`, `LivingEntityAccessor`, `EntityAccessor`, `ItemCooldownManagerAccessor`.
|
||||
- Реестр: `resources/lovisual.mixins.json`. `LoVisualMixinPlugin` — условная загрузка.
|
||||
- Правило: «никаких мёртвых миксинов» — добавляйте миксин только если реально нужен.
|
||||
- `mixininterface/` — интерфейсы-протуберанцы для доступа к данным из миксинов (`IGuiGraphics`, `IEntity`).
|
||||
|
||||
---
|
||||
|
||||
## 14. Чек-лист нового модуля
|
||||
|
||||
1. `@ModuleInfo` на классе в `features/module/modules/<категория>/`.
|
||||
2. Унаследовать `Module`, переопределить `onEnable/onDisable/onTick` и т.д.
|
||||
3. Настройки — поля с хелперами (`num`, `bool`, `mode`, `color`, `visibleWhen`…).
|
||||
4. Если нужен рендер — вернуть `getHudPhase()`/`getWorldPhase()` и реализовать
|
||||
`onRenderHudEngine`/`onRenderWorldEngine`.
|
||||
5. Если нужны события — методы с `@EventHandler`.
|
||||
6. `i18n`: добавить `module.<id>.name/description` и ключи настроек в `en_us.json`/`ru_ru.json`.
|
||||
7. Проверки: `./gradlew build` (сборка), `./gradlew test` (тесты), для чистой логики — JUnit.
|
||||
8. Файл ≤200 строк — иначе вынести логику в хелпер-классы в том же пакете.
|
||||
|
||||
**Не делайте:**
|
||||
- `static final Minecraft` в полях → только ленивый `Minecraft.getInstance()`.
|
||||
- `System.currentTimeMillis()` в модулях → `GameClock.millis()`.
|
||||
- this-escape в конструкторе, `#[allow(dead_code)]`-аналогов (мёртвый код удаляется),
|
||||
мёртвые миксины.
|
||||
|
|
@ -11,6 +11,7 @@ import dev.loki.lovisual.features.command.CommandOutput;
|
|||
import dev.loki.lovisual.render.engine.svg.SvgRenderOptions;
|
||||
import dev.loki.lovisual.render.engine.text.TextGlyphFallback;
|
||||
import dev.loki.lovisual.render.engine.text.TextRenderer;
|
||||
import dev.loki.lovisual.util.time.GameClock;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
|
@ -129,13 +130,21 @@ final class BetterChatGlyphRenderer {
|
|||
tr.render(text.toString(), runX - shadowOffset, textY, BetterChatRenderer.TMP_COLOR, false);
|
||||
tr.render(text.toString(), runX, textY + shadowOffset, BetterChatRenderer.TMP_COLOR, false);
|
||||
tr.render(text.toString(), runX, textY - shadowOffset, BetterChatRenderer.TMP_COLOR, false);
|
||||
|
||||
long now = GameClock.millis();
|
||||
float flow = (now % 2600L) / 2600.0f;
|
||||
float breath = 0.5f + 0.5f * (float) Math.sin(now / 5200.0 * Math.PI * 2.0);
|
||||
tr.renderQuadGradient(text.toString(), runX, textY, (idx, cp, x0, y0, x1, y1, out) -> {
|
||||
float t0 = (float) ((x0 - runX) / runW);
|
||||
float t1 = (float) ((x1 - runX) / runW);
|
||||
int leftTop = BetterChatDrawUtil.mulAlpha(CommandOutput.mixRgb(start, end, t0), alpha);
|
||||
int rightTop = BetterChatDrawUtil.mulAlpha(CommandOutput.mixRgb(start, end, t1), alpha);
|
||||
int leftBottom = BetterChatDrawUtil.mulAlpha(CommandOutput.mixRgb(start, bottomMix, t0), alpha);
|
||||
int rightBottom = BetterChatDrawUtil.mulAlpha(CommandOutput.mixRgb(start, bottomMix, t1), alpha);
|
||||
float t0 = (float) ((x0 - runX) / runW) + flow;
|
||||
float t1 = (float) ((x1 - runX) / runW) + flow;
|
||||
if (t0 > 1.0f) t0 -= 1.0f;
|
||||
if (t1 > 1.0f) t1 -= 1.0f;
|
||||
int base0 = CommandOutput.mixRgb(start, end, t0);
|
||||
int base1 = CommandOutput.mixRgb(start, end, t1);
|
||||
int leftTop = BetterChatDrawUtil.mulAlpha(mixBreath(base0, breath), alpha);
|
||||
int rightTop = BetterChatDrawUtil.mulAlpha(mixBreath(base1, breath), alpha);
|
||||
int leftBottom = BetterChatDrawUtil.mulAlpha(mixBreath(CommandOutput.mixRgb(start, bottomMix, t0), breath), alpha);
|
||||
int rightBottom = BetterChatDrawUtil.mulAlpha(mixBreath(CommandOutput.mixRgb(start, bottomMix, t1), breath), alpha);
|
||||
out[0] = leftTop;
|
||||
out[1] = leftBottom;
|
||||
out[2] = rightBottom;
|
||||
|
|
@ -145,4 +154,8 @@ final class BetterChatGlyphRenderer {
|
|||
tr.end();
|
||||
}
|
||||
}
|
||||
|
||||
private static int mixBreath(int rgb, float breath) {
|
||||
return CommandOutput.mixRgb(rgb, 0xFFFFFF, 0.06f * breath);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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.chat;
|
||||
|
||||
import com.mojang.brigadier.suggestion.Suggestion;
|
||||
import dev.loki.lovisual.features.command.CommandManager;
|
||||
import dev.loki.lovisual.mixins.accessors.ChatScreenAccessor;
|
||||
import dev.loki.lovisual.util.screen.ClientScreen;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.components.EditBox;
|
||||
import net.minecraft.client.gui.screens.ChatScreen;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Tab / arrow-key cycling through the custom {@code @}-command suggestion popup.
|
||||
* Vanilla CommandSuggestions only drives suggestions for {@code /} commands, so our
|
||||
* {@code @} popup needs its own keyboard handling: Up/Down move the highlight, Tab applies.
|
||||
*/
|
||||
public final class BetterChatTabHandler {
|
||||
private BetterChatTabHandler() {
|
||||
}
|
||||
|
||||
private static String lastInput = "";
|
||||
private static int lastSelection = -1;
|
||||
private static int lastTotal = 0;
|
||||
|
||||
public static boolean handleKey(KeyEvent input) {
|
||||
if (input == null) return false;
|
||||
boolean down = input.isDown();
|
||||
boolean up = input.isUp();
|
||||
if (!down && !up && !input.isCycleFocus()) return false;
|
||||
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (!(ClientScreen.current() instanceof ChatScreen chatScreen)) return false;
|
||||
EditBox field = ((ChatScreenAccessor) chatScreen).getChatField();
|
||||
if (field == null) return false;
|
||||
String text = field.getValue();
|
||||
if (text == null || text.isEmpty()) return false;
|
||||
if (BetterChatSuggestionState.commandPrefix(text) != '@') return false;
|
||||
|
||||
int cursor = field.getCursorPosition();
|
||||
List<Suggestion> suggestions = CommandManager.suggest(text, cursor);
|
||||
if (suggestions == null || suggestions.isEmpty()) return false;
|
||||
int total = suggestions.size();
|
||||
if (!text.equals(lastInput) || lastTotal != total) {
|
||||
lastInput = text;
|
||||
lastTotal = total;
|
||||
lastSelection = -1;
|
||||
}
|
||||
|
||||
if (down || up) {
|
||||
int delta = down ? 1 : -1;
|
||||
int selection = lastSelection < 0 ? (delta > 0 ? 0 : total - 1) : (lastSelection + delta) % total;
|
||||
if (selection < 0) selection += total;
|
||||
lastSelection = selection;
|
||||
trackSelection(suggestions, selection);
|
||||
return true;
|
||||
}
|
||||
|
||||
int selection = lastSelection < 0 ? 0 : (lastSelection + (input.hasShiftDown() ? -1 : 1)) % total;
|
||||
if (selection < 0) selection += total;
|
||||
lastSelection = selection;
|
||||
|
||||
Suggestion suggestion = suggestions.get(selection);
|
||||
try {
|
||||
field.setValue(suggestion.apply(text));
|
||||
BetterChatEditBoxUtil.setCursorSafe(field, field.getValue().length());
|
||||
trackSelection(suggestions, selection);
|
||||
} catch (Exception ignored) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void trackSelection(List<Suggestion> suggestions, int selection) {
|
||||
BetterChatRenderer.suggestEntries = suggestions;
|
||||
BetterChatRenderer.suggestCurrentSelection = selection;
|
||||
BetterChatRenderer.suggestCurrentStart = 0;
|
||||
BetterChatRenderer.lastSnapshot = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.visuals;
|
||||
package dev.loki.lovisual.features.module.modules.combat;
|
||||
|
||||
import dev.loki.lovisual.util.time.GameClock;
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer;
|
|||
import dev.loki.lovisual.render.engine.uniform.impl.PostProcessUniforms;
|
||||
|
||||
//todo Description
|
||||
@ModuleInfo(id = "damagetint", displayName = "DamageTint", category = ModuleCategory.VISUALS)
|
||||
@ModuleInfo(id = "damagetint", displayName = "DamageTint", category = ModuleCategory.COMBAT)
|
||||
public class DamageTint extends Module implements PostProcessPass {
|
||||
|
||||
private static final String SETTING_START_HP = "start_hp";
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.visuals;
|
||||
package dev.loki.lovisual.features.module.modules.combat;
|
||||
|
||||
import com.mojang.blaze3d.pipeline.RenderPipeline;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
|
|
@ -69,7 +69,7 @@ import java.util.List;
|
|||
import java.util.Locale;
|
||||
|
||||
//todo Description
|
||||
@ModuleInfo(id = "predictions", displayName = "Predictions", category = ModuleCategory.VISUALS)
|
||||
@ModuleInfo(id = "predictions", displayName = "Predictions", category = ModuleCategory.COMBAT)
|
||||
public class Predictions extends Module {
|
||||
private static final String SETTING_LINE_COLOR = "line_color";
|
||||
private static final String SETTING_PEARL_CUSTOM_COLOR = "pearl_custom_color";
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.visuals;
|
||||
package dev.loki.lovisual.features.module.modules.combat;
|
||||
|
||||
import dev.loki.lovisual.util.time.GameClock;
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ import java.util.List;
|
|||
import java.util.function.IntFunction;
|
||||
|
||||
//todo Description
|
||||
@ModuleInfo(id = "targetesp", displayName = "TargetESP", aliases = {"target", "targetrender"}, category = ModuleCategory.VISUALS)
|
||||
@ModuleInfo(id = "targetesp", displayName = "TargetESP", aliases = {"target", "targetrender"}, category = ModuleCategory.COMBAT)
|
||||
public class TargetESP extends Module {
|
||||
|
||||
private static final String SETTING_MODE = "mode";
|
||||
|
|
@ -1,856 +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.module.modules.visuals;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import dev.loki.lovisual.config.values.*;
|
||||
import dev.loki.lovisual.features.module.*;
|
||||
import dev.loki.lovisual.features.module.Module;
|
||||
import dev.loki.lovisual.util.block.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import dev.loki.lovisual.config.common.CommonSettingSchemas;
|
||||
import dev.loki.lovisual.config.values.*;
|
||||
import dev.loki.lovisual.features.gui.clickgui.settings.TextListSetting;
|
||||
import dev.loki.lovisual.features.module.*;
|
||||
import dev.loki.lovisual.features.module.modules.misc.DefineTarget;
|
||||
import dev.loki.lovisual.render.engine.RenderState;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||
import dev.loki.lovisual.util.block.*;
|
||||
import dev.loki.lovisual.util.block.bed.BedBlockUtil;
|
||||
import dev.loki.lovisual.util.block.bed.BedwarsTeamColorUtil;
|
||||
import dev.loki.lovisual.util.block.bed.SelfBedMode;
|
||||
import dev.loki.lovisual.util.block.bed.SelfBedTracker;
|
||||
import dev.loki.lovisual.util.logging.DebugLog;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
//todo Description
|
||||
@ModuleInfo(
|
||||
id = "bedwarsesp",
|
||||
displayName = "BedwarsESP",
|
||||
category = ModuleCategory.VISUALS
|
||||
)
|
||||
public class BedwarsESP extends Module {
|
||||
//todo не доделан
|
||||
|
||||
private static final int SECTION_SCAN_BUDGET_PER_TICK = 4;
|
||||
private static final int SECTION_SCAN_REFRESH_TICKS = 40;
|
||||
|
||||
private final Minecraft mc = Minecraft.getInstance();
|
||||
private final WorldSectionBlockScanner sectionScanner = new WorldSectionBlockScanner();
|
||||
|
||||
private final EnumValue<BlockScanMode> modeValue =
|
||||
enumCommon(
|
||||
"bedwars_esp_mode",
|
||||
"mode",
|
||||
CommonSettingSchemas.ESP_SCAN_MODE,
|
||||
BlockScanMode.LOS,
|
||||
BlockScanMode.LOS,
|
||||
BlockScanMode.UNRESTRICTED
|
||||
);
|
||||
|
||||
private final BooleanValue limitDistanceValue =
|
||||
boolCommon(
|
||||
"bedwars_esp_limit_distance",
|
||||
"limit_distance",
|
||||
CommonSettingSchemas.ESP_LIMIT_DISTANCE,
|
||||
true
|
||||
);
|
||||
|
||||
private final NumberValue<Integer> maxDistanceValue =
|
||||
visibleWhen(numCommon(
|
||||
"bedwars_esp_max_distance",
|
||||
"max_distance",
|
||||
CommonSettingSchemas.ESP_MAX_DISTANCE,
|
||||
64,
|
||||
1,
|
||||
256
|
||||
), limitDistanceValue::get);
|
||||
|
||||
private final NumberValue<Integer> maxTargetsValue =
|
||||
numCommon(
|
||||
"bedwars_esp_max_targets",
|
||||
"max_targets",
|
||||
CommonSettingSchemas.ESP_MAX_TARGETS,
|
||||
32,
|
||||
1,
|
||||
256
|
||||
);
|
||||
|
||||
private final BlockScanContext scanContext = new BlockScanContext(mc, null);
|
||||
|
||||
private final BooleanValue useTracersValue =
|
||||
boolCommon(
|
||||
"bedwars_esp_use_tracers",
|
||||
"use_tracers",
|
||||
CommonSettingSchemas.ESP_USE_TRACERS,
|
||||
false
|
||||
);
|
||||
|
||||
private final NumberValue<Float> lineWidth =
|
||||
numCommon(
|
||||
"bedwars_esp_line_width",
|
||||
"line_width",
|
||||
CommonSettingSchemas.ESP_LINE_WIDTH,
|
||||
1.5f,
|
||||
0.5f,
|
||||
4.0f
|
||||
);
|
||||
|
||||
private final EnumValue<BlockOutlineShapeMode> outlineShapeMode =
|
||||
enumCommon(
|
||||
"bedwars_esp_outline_shape",
|
||||
"outline_shape",
|
||||
CommonSettingSchemas.ESP_OUTLINE_SHAPE,
|
||||
BlockOutlineShapeMode.FULL_BLOCK,
|
||||
BlockOutlineShapeMode.values()
|
||||
);
|
||||
|
||||
private final EnumValue<SelfBedMode> selfBedMode =
|
||||
enumMode(
|
||||
"bedwars_esp_self_bed",
|
||||
SelfBedMode.NONE,
|
||||
SelfBedMode.values()
|
||||
);
|
||||
private final NumberValue<Float> spawnBedDistance =
|
||||
visibleWhen(num(
|
||||
"bedwars_esp_spawn_bed_distance",
|
||||
"spawn_bed_distance",
|
||||
24.0f,
|
||||
16.0f,
|
||||
48.0f
|
||||
), () -> selfBedMode.get() == SelfBedMode.SPAWN_LOCATION);
|
||||
|
||||
private final NumberValue<Integer> maxLayers =
|
||||
num(
|
||||
"bedwars_esp_max_layers",
|
||||
"max_layers",
|
||||
1,
|
||||
1,
|
||||
5
|
||||
);
|
||||
|
||||
private final BooleanValue showOpeningsValue =
|
||||
bool(
|
||||
"bedwars_esp_show_shell",
|
||||
"show_openings",
|
||||
true
|
||||
);
|
||||
|
||||
private final RGBAColorValue selfBedColor =
|
||||
color(
|
||||
"bedwars_esp_self_bed_color",
|
||||
"self_bed_color",
|
||||
"#8026FF71"
|
||||
);
|
||||
|
||||
private final RGBAColorValue otherBedColor =
|
||||
color(
|
||||
"bedwars_esp_other_bed_color",
|
||||
"other_bed_color",
|
||||
"#80FF6262"
|
||||
);
|
||||
|
||||
private final RGBAColorValue selfOpeningColor =
|
||||
visibleWhen(color(
|
||||
"bedwars_esp_self_shell_color",
|
||||
"self_opening_color",
|
||||
"#4026BFFF"
|
||||
), showOpeningsValue::get);
|
||||
|
||||
private final RGBAColorValue otherOpeningColor =
|
||||
visibleWhen(color(
|
||||
"bedwars_esp_other_shell_color",
|
||||
"other_opening_color",
|
||||
"#40FFC247"
|
||||
), showOpeningsValue::get);
|
||||
private final Map<BlockPos, ObservedBed> observedBedStates = new ConcurrentHashMap<>();
|
||||
private final Map<BlockPos, BlockPos> bedPartToKey = new ConcurrentHashMap<>();
|
||||
private final Set<Long> loggedBeds = ConcurrentHashMap.newKeySet();
|
||||
private volatile Map<BlockPos, BedRenderTarget> renderTargets = new ConcurrentHashMap<>();
|
||||
private ClientLevel lastWorld;
|
||||
private BlockScanMode lastMode;
|
||||
private int lastScanSignature = Integer.MIN_VALUE;
|
||||
private int sectionScanRefreshTicks;
|
||||
|
||||
private static AABB createFaceBox(BlockPos bedPos, net.minecraft.core.Direction face) {
|
||||
final double inset = 0.18;
|
||||
final double depth = 0.045;
|
||||
final double eps = 0.0025;
|
||||
double minX = bedPos.getX();
|
||||
double minY = bedPos.getY();
|
||||
double minZ = bedPos.getZ();
|
||||
double maxX = minX + 1.0;
|
||||
double maxY = minY + 1.0;
|
||||
double maxZ = minZ + 1.0;
|
||||
|
||||
return switch (face) {
|
||||
case DOWN -> new AABB(minX + inset, minY - eps, minZ + inset, maxX - inset, minY + depth, maxZ - inset);
|
||||
case UP -> new AABB(minX + inset, maxY - depth, minZ + inset, maxX - inset, maxY + eps, maxZ - inset);
|
||||
case NORTH -> new AABB(minX + inset, minY + inset, minZ - eps, maxX - inset, maxY - inset, minZ + depth);
|
||||
case SOUTH -> new AABB(minX + inset, minY + inset, maxZ - depth, maxX - inset, maxY - inset, maxZ + eps);
|
||||
case WEST -> new AABB(minX - eps, minY + inset, minZ + inset, minX + depth, maxY - inset, maxZ - inset);
|
||||
case EAST -> new AABB(maxX - depth, minY + inset, minZ + inset, maxX + eps, maxY - inset, maxZ - inset);
|
||||
};
|
||||
}
|
||||
|
||||
private static int multiplyAlpha(int argb, float factor) {
|
||||
int alpha = (argb >>> 24) & 0xFF;
|
||||
int nextAlpha = Math.max(0, Math.min(255, Math.round(alpha * factor)));
|
||||
return (argb & 0x00FFFFFF) | (nextAlpha << 24);
|
||||
}
|
||||
|
||||
private static int emphasizeAlpha(int argb) {
|
||||
int alpha = (argb >>> 24) & 0xFF;
|
||||
int nextAlpha = Math.max(alpha, 170);
|
||||
return (argb & 0x00FFFFFF) | (Math.min(255, nextAlpha) << 24);
|
||||
}
|
||||
|
||||
private static void addFilledBox(Renderer3D renderer, AABB box, int argb) {
|
||||
int a = (argb >>> 24) & 0xFF;
|
||||
int r = (argb >>> 16) & 0xFF;
|
||||
int g = (argb >>> 8) & 0xFF;
|
||||
int b = argb & 0xFF;
|
||||
|
||||
double minX = box.minX;
|
||||
double minY = box.minY;
|
||||
double minZ = box.minZ;
|
||||
double maxX = box.maxX;
|
||||
double maxY = box.maxY;
|
||||
double maxZ = box.maxZ;
|
||||
|
||||
renderer.quad(minX, minY, minZ, maxX, minY, minZ, maxX, minY, maxZ, minX, minY, maxZ, r, g, b, a);
|
||||
renderer.quad(minX, maxY, minZ, minX, maxY, maxZ, maxX, maxY, maxZ, maxX, maxY, minZ, r, g, b, a);
|
||||
renderer.quad(minX, minY, maxZ, maxX, minY, maxZ, maxX, maxY, maxZ, minX, maxY, maxZ, r, g, b, a);
|
||||
renderer.quad(minX, minY, minZ, minX, maxY, minZ, maxX, maxY, minZ, maxX, minY, minZ, r, g, b, a);
|
||||
renderer.quad(maxX, minY, minZ, maxX, maxY, minZ, maxX, maxY, maxZ, maxX, minY, maxZ, r, g, b, a);
|
||||
renderer.quad(minX, minY, minZ, minX, minY, maxZ, minX, maxY, maxZ, minX, maxY, minZ, r, g, b, a);
|
||||
}
|
||||
|
||||
private static void addOutlineBox(Renderer3D renderer, AABB box, int argb) {
|
||||
int a = (argb >>> 24) & 0xFF;
|
||||
int r = (argb >>> 16) & 0xFF;
|
||||
int g = (argb >>> 8) & 0xFF;
|
||||
int b = argb & 0xFF;
|
||||
|
||||
double minX = box.minX;
|
||||
double minY = box.minY;
|
||||
double minZ = box.minZ;
|
||||
double maxX = box.maxX;
|
||||
double maxY = box.maxY;
|
||||
double maxZ = box.maxZ;
|
||||
|
||||
renderer.line(minX, minY, minZ, maxX, minY, minZ, r, g, b, a);
|
||||
renderer.line(maxX, minY, minZ, maxX, minY, maxZ, r, g, b, a);
|
||||
renderer.line(maxX, minY, maxZ, minX, minY, maxZ, r, g, b, a);
|
||||
renderer.line(minX, minY, maxZ, minX, minY, minZ, r, g, b, a);
|
||||
|
||||
renderer.line(minX, maxY, minZ, maxX, maxY, minZ, r, g, b, a);
|
||||
renderer.line(maxX, maxY, minZ, maxX, maxY, maxZ, r, g, b, a);
|
||||
renderer.line(maxX, maxY, maxZ, minX, maxY, maxZ, r, g, b, a);
|
||||
renderer.line(minX, maxY, maxZ, minX, maxY, minZ, r, g, b, a);
|
||||
|
||||
renderer.line(minX, minY, minZ, minX, maxY, minZ, r, g, b, a);
|
||||
renderer.line(maxX, minY, minZ, maxX, maxY, minZ, r, g, b, a);
|
||||
renderer.line(maxX, minY, maxZ, maxX, maxY, maxZ, r, g, b, a);
|
||||
renderer.line(minX, minY, maxZ, minX, maxY, maxZ, r, g, b, a);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
resetRuntimeState();
|
||||
scanContext.refresh();
|
||||
BlockObservationHub.requestSodiumRebuild(mc, scanBlockRadius());
|
||||
requestLocalSectionScan();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTick() {
|
||||
if (!isEnabled() || mc.level == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastWorld != mc.level) {
|
||||
if (lastWorld != null) {
|
||||
resetRuntimeState();
|
||||
RenderSectionBlockScanner.clear();
|
||||
}
|
||||
lastWorld = mc.level;
|
||||
}
|
||||
|
||||
scanContext.refresh();
|
||||
if (mc.player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
BlockScanMode mode = modeValue.get();
|
||||
if (lastMode != mode) {
|
||||
clearRenderedBeds();
|
||||
lastMode = mode;
|
||||
requestLocalSectionScan();
|
||||
}
|
||||
|
||||
maintainSectionScanQueue();
|
||||
drainSectionScanQueue();
|
||||
|
||||
reconcileObservedBeds("cached");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
resetRuntimeState();
|
||||
}
|
||||
|
||||
private void resetRuntimeState() {
|
||||
clearTargets();
|
||||
lastMode = null;
|
||||
lastWorld = mc.level;
|
||||
lastScanSignature = Integer.MIN_VALUE;
|
||||
sectionScanRefreshTicks = 0;
|
||||
sectionScanner.reset();
|
||||
}
|
||||
|
||||
private void clearTargets() {
|
||||
clearRenderedBeds();
|
||||
observedBedStates.clear();
|
||||
}
|
||||
|
||||
private void clearRenderedBeds() {
|
||||
renderTargets = new ConcurrentHashMap<>();
|
||||
bedPartToKey.clear();
|
||||
loggedBeds.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public WorldPhase getWorldPhase() {
|
||||
return WorldPhase.END_MAIN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRenderWorld(PoseStack matrices, SubmitNodeCollector consumers) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
|
||||
if (!isEnabled() || mc.level == null || mc.player == null) return;
|
||||
|
||||
float previousLineWidth = RenderState.lineWidth;
|
||||
RenderState.lineWidth = Math.max(0.5f, lineWidth.get());
|
||||
try {
|
||||
DefineTarget defineTarget = Modules.get(DefineTarget.class);
|
||||
boolean useTeamColors = defineTarget != null && defineTarget.useBedwarsTeamColors();
|
||||
for (BedRenderTarget target : renderTargets.values()) {
|
||||
int bedColor = target.self() ? selfBedColor.getArgb() : otherBedColor.getArgb();
|
||||
if (useTeamColors && target.teamColorRgb() != -1) {
|
||||
bedColor = BedwarsTeamColorUtil.replaceRgb(bedColor, target.teamColorRgb());
|
||||
}
|
||||
|
||||
for (BlockPos bedPart : target.bedParts()) {
|
||||
BlockState state = mc.level.getBlockState(bedPart);
|
||||
if (!BedBlockUtil.isBed(state)) {
|
||||
continue;
|
||||
}
|
||||
BlockOutlineRenderer.render(renderer, mc.level, bedPart, state, outlineShapeMode.get(), emphasizeAlpha(bedColor));
|
||||
}
|
||||
|
||||
if (!showOpeningsValue.get()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int openingColor = target.self() ? selfOpeningColor.getArgb() : otherOpeningColor.getArgb();
|
||||
if (useTeamColors && target.teamColorRgb() != -1) {
|
||||
openingColor = BedwarsTeamColorUtil.replaceRgb(openingColor, target.teamColorRgb());
|
||||
}
|
||||
|
||||
for (FaceMarker opening : target.openingMarkers()) {
|
||||
addFilledBox(renderer, opening.box(), multiplyAlpha(openingColor, 0.85f));
|
||||
addOutlineBox(renderer, opening.box(), emphasizeAlpha(openingColor));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
RenderState.lineWidth = previousLineWidth;
|
||||
}
|
||||
|
||||
renderTracers(renderer, tickDelta);
|
||||
}
|
||||
|
||||
public void acceptSodiumBlockState(int x, int y, int z, BlockState state) {
|
||||
if (!isEnabled() || !BedBlockUtil.isBed(state)) {
|
||||
return;
|
||||
}
|
||||
recordObservedBlock(new BlockPos(x, y, z), state, false, RenderStatus.HIDDEN);
|
||||
}
|
||||
|
||||
public void acceptSodiumRenderedBlockState(BlockPos pos, BlockState state) {
|
||||
if (!isEnabled() || !BedBlockUtil.isBed(state)) {
|
||||
return;
|
||||
}
|
||||
recordObservedBlock(pos, state, false, RenderStatus.VISIBLE);
|
||||
}
|
||||
|
||||
public void acceptWorldBlockUpdate(BlockPos pos, BlockState state) {
|
||||
if (!isEnabled()) {
|
||||
return;
|
||||
}
|
||||
recordObservedBlock(pos, state, true);
|
||||
}
|
||||
|
||||
private void recordObservedBlock(BlockPos rawPos, BlockState state) {
|
||||
recordObservedBlock(rawPos, state, false, RenderStatus.PRESERVE);
|
||||
}
|
||||
|
||||
private void recordObservedBlock(BlockPos rawPos, BlockState state, boolean forgetScannedSection) {
|
||||
recordObservedBlock(rawPos, state, forgetScannedSection, RenderStatus.PRESERVE);
|
||||
}
|
||||
|
||||
private void recordObservedBlock(BlockPos rawPos,
|
||||
BlockState state,
|
||||
boolean forgetScannedSection,
|
||||
RenderStatus renderStatus) {
|
||||
if (rawPos == null || state == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
BlockPos pos = rawPos.immutable();
|
||||
if (forgetScannedSection) {
|
||||
sectionScanner.forget(pos);
|
||||
}
|
||||
if (!BedBlockUtil.isBed(state)) {
|
||||
observedBedStates.remove(pos);
|
||||
removeBedByPart(pos);
|
||||
return;
|
||||
}
|
||||
|
||||
RenderSectionBlockScanner.recordBlock(pos, state);
|
||||
observedBedStates.compute(pos, (ignored, previous) -> {
|
||||
boolean rendered = switch (renderStatus) {
|
||||
case PRESERVE -> previous != null && previous.rendered();
|
||||
case HIDDEN -> false;
|
||||
case VISIBLE -> true;
|
||||
};
|
||||
return new ObservedBed(state, rendered);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateObservedBed(BlockPos pos, BlockState state, String source) {
|
||||
if (mc.level == null || mc.player == null || !withinDistance(pos)) {
|
||||
removeBedByPart(pos);
|
||||
return;
|
||||
}
|
||||
|
||||
ObservedBed observed = observedBedStates.get(pos);
|
||||
boolean visible = observed != null && observed.rendered();
|
||||
if (!acceptsMode(pos, visible)) {
|
||||
removeBedByPart(pos);
|
||||
return;
|
||||
}
|
||||
|
||||
BlockPos key = BedBlockUtil.canonicalBedPos(pos, state);
|
||||
if (key == null) {
|
||||
removeBedByPart(pos);
|
||||
return;
|
||||
}
|
||||
key = key.immutable();
|
||||
if (!renderTargets.containsKey(key) && renderTargets.size() >= maxTargetsValue.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<BlockPos, BedRenderTarget> built = buildBedTargets(List.of(
|
||||
new RenderSectionBlockScanner.ScanResult(pos, state, visible)
|
||||
));
|
||||
BedRenderTarget target = built.get(key);
|
||||
if (target == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
BedRenderTarget previous = renderTargets.put(key, target);
|
||||
if (previous != null) {
|
||||
for (BlockPos bedPart : previous.bedParts()) {
|
||||
bedPartToKey.remove(bedPart);
|
||||
}
|
||||
}
|
||||
for (BlockPos bedPart : target.bedParts()) {
|
||||
bedPartToKey.put(bedPart, key);
|
||||
}
|
||||
|
||||
if (loggedBeds.add(key.asLong())) {
|
||||
DebugLog.info(
|
||||
"[BedwarsESP] detected %s at %s visible=%s source=%s mode=%s",
|
||||
BuiltInRegistries.BLOCK.getKey(state.getBlock()),
|
||||
key.toShortString(),
|
||||
visible,
|
||||
source,
|
||||
modeValue.get().getId()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void reconcileObservedBeds(String source) {
|
||||
for (Map.Entry<BlockPos, ObservedBed> entry : observedBedStates.entrySet()) {
|
||||
BlockPos pos = entry.getKey();
|
||||
ObservedBed observed = entry.getValue();
|
||||
BlockState state = observed.state();
|
||||
if (mc.level != null && withinDistance(pos)) {
|
||||
BlockState currentState = mc.level.getBlockState(pos);
|
||||
if (BedBlockUtil.isBed(currentState)) {
|
||||
if (currentState != state) {
|
||||
observedBedStates.put(pos, new ObservedBed(currentState, observed.rendered()));
|
||||
state = currentState;
|
||||
}
|
||||
} else {
|
||||
observedBedStates.remove(pos);
|
||||
removeBedByPart(pos);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
updateObservedBed(pos, state, source);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeBedByPart(BlockPos pos) {
|
||||
BlockPos key = bedPartToKey.remove(pos);
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
BedRenderTarget removed = renderTargets.remove(key);
|
||||
loggedBeds.remove(key.asLong());
|
||||
if (removed != null) {
|
||||
for (BlockPos bedPart : removed.bedParts()) {
|
||||
bedPartToKey.remove(bedPart);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void maintainSectionScanQueue() {
|
||||
if (mc.level == null || mc.player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int signature = currentScanSignature();
|
||||
if (signature != lastScanSignature || ++sectionScanRefreshTicks >= SECTION_SCAN_REFRESH_TICKS) {
|
||||
lastScanSignature = signature;
|
||||
sectionScanRefreshTicks = 0;
|
||||
requestLocalSectionScan();
|
||||
}
|
||||
}
|
||||
|
||||
private void requestLocalSectionScan() {
|
||||
if (mc.level == null || mc.player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int radius = scanBlockRadius();
|
||||
int minY;
|
||||
int maxY;
|
||||
if (limitDistanceValue.get()) {
|
||||
int eyeY = (int) Math.floor(mc.player.getEyePosition().y);
|
||||
minY = eyeY - radius;
|
||||
maxY = eyeY + radius;
|
||||
} else {
|
||||
minY = mc.level.getMinY();
|
||||
maxY = mc.level.getMaxY();
|
||||
}
|
||||
|
||||
sectionScanner.enqueueAround(mc, radius, minY, maxY);
|
||||
}
|
||||
|
||||
private void drainSectionScanQueue() {
|
||||
sectionScanner.drain(
|
||||
mc,
|
||||
SECTION_SCAN_BUDGET_PER_TICK,
|
||||
BedBlockUtil::isBed,
|
||||
this::recordObservedBlock
|
||||
);
|
||||
}
|
||||
|
||||
private int scanBlockRadius() {
|
||||
return limitDistanceValue.get() ? Math.max(1, maxDistanceValue.get()) : 256;
|
||||
}
|
||||
|
||||
private int currentScanSignature() {
|
||||
int centerChunkX = SectionPos.blockToSectionCoord(mc.player.getBlockX());
|
||||
int centerChunkZ = SectionPos.blockToSectionCoord(mc.player.getBlockZ());
|
||||
int centerSectionY = SectionPos.blockToSectionCoord(mc.player.getBlockY());
|
||||
int result = 17;
|
||||
result = 31 * result + centerChunkX;
|
||||
result = 31 * result + centerChunkZ;
|
||||
result = 31 * result + centerSectionY;
|
||||
result = 31 * result + (limitDistanceValue.get() ? 1 : 0);
|
||||
result = 31 * result + maxDistanceValue.get();
|
||||
result = 31 * result + maxLayers.get();
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean withinDistance(BlockPos pos) {
|
||||
if (!limitDistanceValue.get() || mc.player == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Vec3 eye = mc.player.getEyePosition();
|
||||
double dx = pos.getX() + 0.5 - eye.x;
|
||||
double dy = pos.getY() + 0.5 - eye.y;
|
||||
double dz = pos.getZ() + 0.5 - eye.z;
|
||||
int maxDistance = maxDistanceValue.get();
|
||||
return dx * dx + dy * dy + dz * dz <= (double) maxDistance * (double) maxDistance;
|
||||
}
|
||||
|
||||
private boolean acceptsMode(BlockPos pos, boolean visible) {
|
||||
return switch (modeValue.get()) {
|
||||
case LOS -> visible;
|
||||
case UNRESTRICTED -> true;
|
||||
};
|
||||
}
|
||||
|
||||
private Map<BlockPos, BedRenderTarget> buildBedTargets(Collection<RenderSectionBlockScanner.ScanResult> scanResults) {
|
||||
Map<BlockPos, MutableBedTarget> grouped = new HashMap<>();
|
||||
for (RenderSectionBlockScanner.ScanResult result : scanResults) {
|
||||
BlockState state = result.state();
|
||||
BlockPos key = BedBlockUtil.canonicalBedPos(result.pos(), state);
|
||||
if (key == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
MutableBedTarget target = grouped.computeIfAbsent(key, ignored -> new MutableBedTarget());
|
||||
target.sourcePos = result.pos();
|
||||
target.sourceState = state;
|
||||
target.visible |= result.visible();
|
||||
Integer teamColorRgb = BedwarsTeamColorUtil.getBedTeamRgb(state);
|
||||
if (teamColorRgb != null) {
|
||||
target.teamColorRgb = teamColorRgb;
|
||||
}
|
||||
target.self |= SelfBedTracker.INSTANCE.isSelfBed(
|
||||
selfBedMode.get(),
|
||||
state,
|
||||
result.pos(),
|
||||
mc.player,
|
||||
spawnBedDistance.get()
|
||||
);
|
||||
target.bedParts.add(result.pos().immutable());
|
||||
|
||||
net.minecraft.core.Direction direction = BedBlockUtil.anotherBedPartDirection(state);
|
||||
if (direction != null) {
|
||||
target.bedParts.add(result.pos().relative(direction).immutable());
|
||||
}
|
||||
}
|
||||
|
||||
Map<BlockPos, BedRenderTarget> next = new HashMap<>(grouped.size());
|
||||
for (Map.Entry<BlockPos, MutableBedTarget> entry : grouped.entrySet()) {
|
||||
MutableBedTarget value = entry.getValue();
|
||||
if (value.sourcePos == null || value.sourceState == null || value.bedParts.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<AABB> bedBoxes = new ArrayList<>(value.bedParts.size());
|
||||
double cx = 0.0;
|
||||
double cy = 0.0;
|
||||
double cz = 0.0;
|
||||
for (BlockPos bedPart : value.bedParts) {
|
||||
bedBoxes.add(new AABB(bedPart));
|
||||
Vec3 center = Vec3.atCenterOf(bedPart);
|
||||
cx += center.x;
|
||||
cy += center.y;
|
||||
cz += center.z;
|
||||
}
|
||||
Vec3 tracerCenter = new Vec3(cx / value.bedParts.size(), cy / value.bedParts.size(), cz / value.bedParts.size());
|
||||
|
||||
List<FaceMarker> openingMarkers = List.of();
|
||||
if (showOpeningsValue.get()) {
|
||||
LinkedHashSet<BlockPos> shellPositions = new LinkedHashSet<>();
|
||||
List<BedBlockUtil.LayeredBlockPos> layerPositions = BedBlockUtil.searchBedLayer(
|
||||
value.sourcePos,
|
||||
value.sourceState,
|
||||
maxLayers.get()
|
||||
);
|
||||
layerPositions.sort(Comparator.comparingInt(BedBlockUtil.LayeredBlockPos::layer));
|
||||
for (BedBlockUtil.LayeredBlockPos layerPos : layerPositions) {
|
||||
shellPositions.add(layerPos.pos());
|
||||
}
|
||||
openingMarkers = collectOpeningMarkers(value.bedParts, shellPositions);
|
||||
}
|
||||
|
||||
next.put(entry.getKey(), new BedRenderTarget(
|
||||
List.copyOf(value.bedParts),
|
||||
List.copyOf(bedBoxes),
|
||||
openingMarkers,
|
||||
tracerCenter,
|
||||
value.self,
|
||||
value.visible,
|
||||
value.teamColorRgb
|
||||
));
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
private List<FaceMarker> collectOpeningMarkers(Collection<BlockPos> bedParts, Collection<BlockPos> shellPositions) {
|
||||
if (bedParts.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
Set<BlockPos> bedSet = new HashSet<>(bedParts);
|
||||
int minX = Integer.MAX_VALUE;
|
||||
int minY = Integer.MAX_VALUE;
|
||||
int minZ = Integer.MAX_VALUE;
|
||||
int maxX = Integer.MIN_VALUE;
|
||||
int maxY = Integer.MIN_VALUE;
|
||||
int maxZ = Integer.MIN_VALUE;
|
||||
|
||||
for (BlockPos pos : bedParts) {
|
||||
minX = Math.min(minX, pos.getX());
|
||||
minY = Math.min(minY, pos.getY());
|
||||
minZ = Math.min(minZ, pos.getZ());
|
||||
maxX = Math.max(maxX, pos.getX());
|
||||
maxY = Math.max(maxY, pos.getY());
|
||||
maxZ = Math.max(maxZ, pos.getZ());
|
||||
}
|
||||
for (BlockPos pos : shellPositions) {
|
||||
minX = Math.min(minX, pos.getX());
|
||||
minY = Math.min(minY, pos.getY());
|
||||
minZ = Math.min(minZ, pos.getZ());
|
||||
maxX = Math.max(maxX, pos.getX());
|
||||
maxY = Math.max(maxY, pos.getY());
|
||||
maxZ = Math.max(maxZ, pos.getZ());
|
||||
}
|
||||
|
||||
minX--;
|
||||
minY--;
|
||||
minZ--;
|
||||
maxX++;
|
||||
maxY++;
|
||||
maxZ++;
|
||||
|
||||
ArrayDeque<BlockPos> queue = new ArrayDeque<>();
|
||||
Set<BlockPos> reachable = new HashSet<>();
|
||||
BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos();
|
||||
|
||||
for (int x = minX; x <= maxX; x++) {
|
||||
for (int y = minY; y <= maxY; y++) {
|
||||
seedOpeningBoundary(queue, reachable, bedSet, mutable.set(x, y, minZ));
|
||||
seedOpeningBoundary(queue, reachable, bedSet, mutable.set(x, y, maxZ));
|
||||
}
|
||||
}
|
||||
for (int x = minX; x <= maxX; x++) {
|
||||
for (int z = minZ; z <= maxZ; z++) {
|
||||
seedOpeningBoundary(queue, reachable, bedSet, mutable.set(x, minY, z));
|
||||
seedOpeningBoundary(queue, reachable, bedSet, mutable.set(x, maxY, z));
|
||||
}
|
||||
}
|
||||
for (int y = minY; y <= maxY; y++) {
|
||||
for (int z = minZ; z <= maxZ; z++) {
|
||||
seedOpeningBoundary(queue, reachable, bedSet, mutable.set(minX, y, z));
|
||||
seedOpeningBoundary(queue, reachable, bedSet, mutable.set(maxX, y, z));
|
||||
}
|
||||
}
|
||||
|
||||
while (!queue.isEmpty()) {
|
||||
BlockPos current = queue.removeFirst();
|
||||
for (net.minecraft.core.Direction direction : BlockScanContext.DIRECTIONS) {
|
||||
mutable.set(current).move(direction);
|
||||
if (mutable.getX() < minX || mutable.getX() > maxX
|
||||
|| mutable.getY() < minY || mutable.getY() > maxY
|
||||
|| mutable.getZ() < minZ || mutable.getZ() > maxZ) {
|
||||
continue;
|
||||
}
|
||||
seedOpeningBoundary(queue, reachable, bedSet, mutable);
|
||||
}
|
||||
}
|
||||
|
||||
LinkedHashSet<FaceKey> openings = new LinkedHashSet<>();
|
||||
for (BlockPos airPos : reachable) {
|
||||
for (net.minecraft.core.Direction direction : BlockScanContext.DIRECTIONS) {
|
||||
BlockPos neighbor = airPos.relative(direction);
|
||||
if (!bedSet.contains(neighbor)) {
|
||||
continue;
|
||||
}
|
||||
openings.add(new FaceKey(neighbor.immutable(), direction.getOpposite()));
|
||||
}
|
||||
}
|
||||
|
||||
if (openings.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<FaceMarker> result = new ArrayList<>(openings.size());
|
||||
for (FaceKey opening : openings) {
|
||||
result.add(new FaceMarker(createFaceBox(opening.pos(), opening.face())));
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private void seedOpeningBoundary(ArrayDeque<BlockPos> queue,
|
||||
Set<BlockPos> reachable,
|
||||
Set<BlockPos> bedSet,
|
||||
BlockPos pos) {
|
||||
if (bedSet.contains(pos) || !scanContext.isPassable(pos)) {
|
||||
return;
|
||||
}
|
||||
BlockPos immutable = pos.immutable();
|
||||
if (reachable.add(immutable)) {
|
||||
queue.addLast(immutable);
|
||||
}
|
||||
}
|
||||
|
||||
private void renderTracers(Renderer3D renderer, float tickDelta) {
|
||||
Tracers tracers = Modules.get(Tracers.class);
|
||||
if (!useTracersValue.get() || tracers == null) return;
|
||||
|
||||
DefineTarget defineTarget = Modules.get(DefineTarget.class);
|
||||
boolean useTeamColors = defineTarget != null && defineTarget.useBedwarsTeamColors();
|
||||
for (BedRenderTarget target : renderTargets.values()) {
|
||||
int color = target.self() ? selfBedColor.getArgb() : otherBedColor.getArgb();
|
||||
if (useTeamColors && target.teamColorRgb() != -1) {
|
||||
color = BedwarsTeamColorUtil.replaceRgb(color, target.teamColorRgb());
|
||||
}
|
||||
tracers.render3DTracersRaw(renderer, tickDelta, List.of(target.tracerCenter()), color);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class MutableBedTarget {
|
||||
private final LinkedHashSet<BlockPos> bedParts = new LinkedHashSet<>();
|
||||
private BlockPos sourcePos;
|
||||
private BlockState sourceState;
|
||||
private boolean self;
|
||||
private boolean visible;
|
||||
private int teamColorRgb = -1;
|
||||
}
|
||||
|
||||
private enum RenderStatus {
|
||||
PRESERVE,
|
||||
HIDDEN,
|
||||
VISIBLE
|
||||
}
|
||||
|
||||
private record ObservedBed(BlockState state, boolean rendered) {
|
||||
}
|
||||
|
||||
private record FaceKey(BlockPos pos, net.minecraft.core.Direction face) {
|
||||
}
|
||||
|
||||
private record FaceMarker(AABB box) {
|
||||
}
|
||||
|
||||
private record BedRenderTarget(
|
||||
List<BlockPos> bedParts,
|
||||
List<AABB> bedBoxes,
|
||||
List<FaceMarker> openingMarkers,
|
||||
Vec3 tracerCenter,
|
||||
boolean self,
|
||||
boolean visible,
|
||||
int teamColorRgb
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -1,835 +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.module.modules.visuals;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import dev.loki.lovisual.config.values.*;
|
||||
import dev.loki.lovisual.features.module.*;
|
||||
import dev.loki.lovisual.features.module.Module;
|
||||
import dev.loki.lovisual.util.block.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.SectionPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import dev.loki.lovisual.config.common.CommonSettingSchemas;
|
||||
import dev.loki.lovisual.config.values.*;
|
||||
import dev.loki.lovisual.features.gui.clickgui.settings.TextListSetting;
|
||||
import dev.loki.lovisual.features.module.*;
|
||||
import dev.loki.lovisual.render.engine.RenderState;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||
import dev.loki.lovisual.util.block.*;
|
||||
import dev.loki.lovisual.util.logging.DebugLog;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
//todo Description
|
||||
@ModuleInfo(
|
||||
id = "blockesp",
|
||||
displayName = "BlockESP",
|
||||
aliases = {"xray", "oreesp", "blocks"},
|
||||
category = ModuleCategory.VISUALS
|
||||
)
|
||||
public class BlockESP extends Module {
|
||||
|
||||
private static final int SECTION_SCAN_BUDGET_PER_TICK = 10;
|
||||
private static final int SECTION_SCAN_BURST_BUDGET_PER_TICK = 64;
|
||||
private static final int SECTION_SCAN_BURST_TICKS = 4;
|
||||
private static final int SECTION_SCAN_REFRESH_TICKS = 40;
|
||||
private static final int SODIUM_CANDIDATE_DRAIN_BUDGET_PER_TICK = 4096;
|
||||
private static final int RECONCILE_BUDGET_PER_TICK = 512;
|
||||
private static final int FULL_RECONCILE_REFRESH_TICKS = 20;
|
||||
private final Minecraft mc = Minecraft.getInstance();
|
||||
private final BlockDeobfuscationScanner deobfuscationScanner = new BlockDeobfuscationScanner();
|
||||
private final WorldSectionBlockScanner sectionScanner = new WorldSectionBlockScanner();
|
||||
|
||||
private final EnumValue<BlockScanMode> modeValue =
|
||||
enumCommon(
|
||||
"blockEspMode",
|
||||
"mode",
|
||||
CommonSettingSchemas.ESP_SCAN_MODE,
|
||||
BlockScanMode.LOS,
|
||||
BlockScanMode.values()
|
||||
);
|
||||
|
||||
private final BooleanValue deobfuscationValue =
|
||||
bool(
|
||||
"blockEspDeobfuscation",
|
||||
"deobfuscation",
|
||||
false
|
||||
);
|
||||
|
||||
private final BooleanValue limitDistanceValue =
|
||||
boolCommon(
|
||||
"blockEspLimitDistance",
|
||||
"limit_distance",
|
||||
CommonSettingSchemas.ESP_LIMIT_DISTANCE,
|
||||
true
|
||||
);
|
||||
|
||||
private final NumberValue<Integer> maxDistanceValue =
|
||||
visibleWhen(numCommon(
|
||||
"blockEspMaxDistance",
|
||||
"max_distance",
|
||||
CommonSettingSchemas.ESP_MAX_DISTANCE,
|
||||
64,
|
||||
1,
|
||||
256
|
||||
), limitDistanceValue::get);
|
||||
|
||||
private final NumberValue<Integer> maxTargetsValue =
|
||||
numCommon(
|
||||
"blockEspMaxTargets",
|
||||
"max_targets",
|
||||
CommonSettingSchemas.ESP_MAX_TARGETS,
|
||||
128,
|
||||
1,
|
||||
4096
|
||||
);
|
||||
|
||||
private final NumberValue<Integer> deobfuscationRadiusValue =
|
||||
visibleWhen(num(
|
||||
"blockEspDeobfuscationRadius",
|
||||
"deobfuscation_radius",
|
||||
5,
|
||||
1,
|
||||
64
|
||||
), deobfuscationValue::get);
|
||||
|
||||
private final NumberValue<Integer> deobfuscationUpValue =
|
||||
visibleWhen(num(
|
||||
"blockEspDeobfuscationUp",
|
||||
"deobfuscation_up",
|
||||
5,
|
||||
1,
|
||||
32
|
||||
), deobfuscationValue::get);
|
||||
|
||||
private final NumberValue<Integer> deobfuscationDownValue =
|
||||
visibleWhen(num(
|
||||
"blockEspDeobfuscationDown",
|
||||
"deobfuscation_down",
|
||||
5,
|
||||
1,
|
||||
32
|
||||
), deobfuscationValue::get);
|
||||
|
||||
private final BooleanValue deobfuscationFastValue =
|
||||
visibleWhen(bool(
|
||||
"blockEspDeobfuscationFast",
|
||||
"deobfuscation_fast",
|
||||
false
|
||||
), deobfuscationValue::get);
|
||||
|
||||
private final ItemIdSetValue searchBlocksValue =
|
||||
itemList(
|
||||
"blockEspSearchBlocks",
|
||||
"search_blocks",
|
||||
TextListSetting.PickerMode.BLOCKS
|
||||
);
|
||||
|
||||
private final BlockScanContext scanContext = new BlockScanContext(mc, searchBlocksValue);
|
||||
|
||||
private final BooleanValue useTracersValue =
|
||||
boolCommon(
|
||||
"blockEspUseTracers",
|
||||
"use_tracers",
|
||||
CommonSettingSchemas.ESP_USE_TRACERS,
|
||||
false
|
||||
);
|
||||
|
||||
private final EnumValue<BlockOutlineShapeMode> outlineShapeMode =
|
||||
enumCommon(
|
||||
"blockEspOutlineShape",
|
||||
"outline_shape",
|
||||
CommonSettingSchemas.ESP_OUTLINE_SHAPE,
|
||||
BlockOutlineShapeMode.FULL_BLOCK,
|
||||
BlockOutlineShapeMode.values()
|
||||
);
|
||||
|
||||
private final NumberValue<Float> lineWidth =
|
||||
numCommon(
|
||||
"blockEspLineWidth",
|
||||
"line_width",
|
||||
CommonSettingSchemas.ESP_LINE_WIDTH,
|
||||
1.5f,
|
||||
0.5f,
|
||||
4.0f
|
||||
);
|
||||
|
||||
private final RGBColorValue visibleColor =
|
||||
colorNoAlpha(
|
||||
"blockEspVisibleColor",
|
||||
"visible_color",
|
||||
"#FF0000"
|
||||
);
|
||||
|
||||
private final RGBColorValue chunkColor =
|
||||
colorNoAlpha(
|
||||
"blockEspChunkColor",
|
||||
"chunk_color",
|
||||
"#FFFF00"
|
||||
);
|
||||
|
||||
private final BooleanValue clusterTracersValue =
|
||||
visibleWhen(bool(
|
||||
"blockEspClusterTracers",
|
||||
"cluster_tracers",
|
||||
false
|
||||
), useTracersValue::get);
|
||||
private final Map<BlockPos, ObservedTarget> observedTargetStates = new ConcurrentHashMap<>();
|
||||
private final Set<Long> loggedTargets = ConcurrentHashMap.newKeySet();
|
||||
private final ArrayDeque<BlockPos> reconcileQueue = new ArrayDeque<>();
|
||||
private final Set<Long> queuedReconcileTargets = ConcurrentHashMap.newKeySet();
|
||||
private volatile Map<BlockPos, Target> renderTargets = new ConcurrentHashMap<>();
|
||||
private ClientLevel lastWorld;
|
||||
private BlockScanMode lastMode;
|
||||
private int lastSearchBlocksHash;
|
||||
private int lastScanSignature = Integer.MIN_VALUE;
|
||||
private int sectionScanRefreshTicks;
|
||||
private int sectionScanBurstTicks;
|
||||
private int fastSnapshotGeneration;
|
||||
private int fullReconcileRefreshTicks;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
resetRuntimeState();
|
||||
scanContext.refresh();
|
||||
lastSearchBlocksHash = currentSearchBlocksHash();
|
||||
publishFastScanSnapshot();
|
||||
BlockObservationHub.requestSodiumRebuild(mc, scanBlockRadius());
|
||||
requestLocalSectionScan(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
BlockEspSodiumCandidateCollector.disable();
|
||||
resetRuntimeState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTick() {
|
||||
if (!isEnabled() || mc.level == null) {
|
||||
BlockEspSodiumCandidateCollector.disable();
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastWorld != mc.level) {
|
||||
if (lastWorld != null) {
|
||||
resetRuntimeState();
|
||||
RenderSectionBlockScanner.clear();
|
||||
}
|
||||
lastWorld = mc.level;
|
||||
}
|
||||
|
||||
scanContext.refresh();
|
||||
if (mc.player == null) {
|
||||
BlockEspSodiumCandidateCollector.disable();
|
||||
return;
|
||||
}
|
||||
publishFastScanSnapshot();
|
||||
|
||||
int searchBlocksHash = currentSearchBlocksHash();
|
||||
if (lastSearchBlocksHash != searchBlocksHash) {
|
||||
clearObservedTargets();
|
||||
deobfuscationScanner.reset();
|
||||
sectionScanner.reset();
|
||||
lastSearchBlocksHash = searchBlocksHash;
|
||||
fastSnapshotGeneration++;
|
||||
publishFastScanSnapshot();
|
||||
BlockObservationHub.requestSodiumRebuild(mc, scanBlockRadius());
|
||||
requestLocalSectionScan(true);
|
||||
}
|
||||
|
||||
BlockScanMode mode = modeValue.get();
|
||||
if (lastMode != mode) {
|
||||
renderTargets = new ConcurrentHashMap<>();
|
||||
loggedTargets.clear();
|
||||
reconcileQueue.clear();
|
||||
queuedReconcileTargets.clear();
|
||||
lastMode = mode;
|
||||
requestLocalSectionScan(true);
|
||||
}
|
||||
|
||||
drainSodiumCandidateQueue();
|
||||
maintainSectionScanQueue();
|
||||
drainSectionScanQueue();
|
||||
|
||||
if (deobfuscationValue.get()) {
|
||||
deobfuscationScanner.tick(
|
||||
mc,
|
||||
1,
|
||||
deobfuscationRadiusValue.get(),
|
||||
deobfuscationUpValue.get(),
|
||||
deobfuscationDownValue.get(),
|
||||
deobfuscationFastValue.get()
|
||||
);
|
||||
mergeDeobfuscatedTargets();
|
||||
}
|
||||
|
||||
maintainReconcileQueue();
|
||||
drainReconcileQueue("cached", RECONCILE_BUDGET_PER_TICK);
|
||||
}
|
||||
|
||||
public void acceptSodiumBlockState(int x, int y, int z, BlockState state) {
|
||||
if (!isEnabled() || !scanContext.isConfiguredTarget(state)) {
|
||||
return;
|
||||
}
|
||||
acceptObservedCandidate(BlockPos.asLong(x, y, z), state, "sodium-direct", RenderStatus.HIDDEN);
|
||||
}
|
||||
|
||||
public void acceptWorldBlockUpdate(BlockPos pos, BlockState state) {
|
||||
if (!isEnabled()) {
|
||||
return;
|
||||
}
|
||||
acceptBlockUpdate(pos, state);
|
||||
}
|
||||
|
||||
private void acceptBlockUpdate(BlockPos pos, BlockState state) {
|
||||
if (pos == null || state == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
sectionScanner.forget(pos);
|
||||
scanContext.refresh();
|
||||
boolean target = scanContext.isConfiguredTarget(state);
|
||||
boolean deobfuscatedPositive = deobfuscationScanner.hasPositiveState(pos);
|
||||
deobfuscationScanner.acceptBlockUpdate(pos, state, scanContext::isConfiguredTarget);
|
||||
|
||||
if (!target && deobfuscatedPositive) {
|
||||
BlockState positiveState = deobfuscationScanner.getPositiveState(pos);
|
||||
if (scanContext.isConfiguredTarget(positiveState)) {
|
||||
recordTargetCandidate(pos, positiveState);
|
||||
updateObservedTarget(pos, positiveState, "deobfuscation-update");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
removeTargetCandidate(pos);
|
||||
return;
|
||||
}
|
||||
|
||||
recordTargetCandidate(pos, state);
|
||||
updateObservedTarget(pos, state, "world-update");
|
||||
if (deobfuscationValue.get()) {
|
||||
DebugLog.info("[BlockESP] deobfuscated %s at %s", BuiltInRegistries.BLOCK.getKey(state.getBlock()), pos.toShortString());
|
||||
}
|
||||
}
|
||||
|
||||
private void resetRuntimeState() {
|
||||
clearObservedTargets();
|
||||
lastMode = null;
|
||||
lastWorld = mc.level;
|
||||
lastSearchBlocksHash = currentSearchBlocksHash();
|
||||
lastScanSignature = Integer.MIN_VALUE;
|
||||
sectionScanRefreshTicks = 0;
|
||||
sectionScanBurstTicks = 0;
|
||||
fullReconcileRefreshTicks = 0;
|
||||
fastSnapshotGeneration++;
|
||||
BlockEspSodiumCandidateCollector.clear();
|
||||
deobfuscationScanner.reset();
|
||||
sectionScanner.reset();
|
||||
}
|
||||
|
||||
private void clearObservedTargets() {
|
||||
renderTargets = new ConcurrentHashMap<>();
|
||||
observedTargetStates.clear();
|
||||
loggedTargets.clear();
|
||||
reconcileQueue.clear();
|
||||
queuedReconcileTargets.clear();
|
||||
BlockEspSodiumCandidateCollector.clear();
|
||||
}
|
||||
|
||||
private void recordTargetCandidate(BlockPos rawPos, BlockState state) {
|
||||
recordTargetCandidate(rawPos, state, RenderStatus.PRESERVE);
|
||||
}
|
||||
|
||||
private void recordTargetCandidate(BlockPos rawPos, BlockState state, RenderStatus renderStatus) {
|
||||
if (rawPos == null || state == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
BlockPos pos = rawPos.immutable();
|
||||
RenderSectionBlockScanner.recordBlock(pos, state);
|
||||
observedTargetStates.compute(pos, (ignored, previous) -> {
|
||||
boolean rendered = switch (renderStatus) {
|
||||
case PRESERVE -> previous != null && previous.rendered();
|
||||
case HIDDEN -> false;
|
||||
case VISIBLE -> true;
|
||||
};
|
||||
return new ObservedTarget(state, rendered);
|
||||
});
|
||||
enqueueReconcile(pos);
|
||||
}
|
||||
|
||||
private void removeTargetCandidate(BlockPos pos) {
|
||||
if (pos == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
observedTargetStates.remove(pos);
|
||||
renderTargets.remove(pos);
|
||||
loggedTargets.remove(pos.asLong());
|
||||
queuedReconcileTargets.remove(pos.asLong());
|
||||
sectionScanner.forget(pos);
|
||||
if (mc.level != null) {
|
||||
RenderSectionBlockScanner.recordBlock(pos, mc.level.getBlockState(pos));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateObservedTarget(BlockPos pos, BlockState state, String source) {
|
||||
if (mc.level == null || mc.player == null || !withinDistance(pos)) {
|
||||
renderTargets.remove(pos);
|
||||
return;
|
||||
}
|
||||
|
||||
ObservedTarget observed = observedTargetStates.get(pos);
|
||||
boolean visible = observed != null && observed.rendered();
|
||||
if (!acceptsMode(pos, visible)) {
|
||||
renderTargets.remove(pos);
|
||||
return;
|
||||
}
|
||||
if (!renderTargets.containsKey(pos) && renderTargets.size() >= maxTargetsValue.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Target target = new Target();
|
||||
target.pos = pos;
|
||||
target.state = state;
|
||||
target.box = new AABB(pos);
|
||||
target.visible = visible;
|
||||
renderTargets.put(pos, target);
|
||||
|
||||
if (loggedTargets.add(pos.asLong())) {
|
||||
DebugLog.info(
|
||||
"[BlockESP] detected %s at %s visible=%s source=%s mode=%s",
|
||||
BuiltInRegistries.BLOCK.getKey(state.getBlock()),
|
||||
pos.toShortString(),
|
||||
visible,
|
||||
source,
|
||||
modeValue.get().getId()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void drainSodiumCandidateQueue() {
|
||||
BlockEspSodiumCandidateCollector.drain(
|
||||
SODIUM_CANDIDATE_DRAIN_BUDGET_PER_TICK,
|
||||
candidate -> {
|
||||
if (candidate.generation() == fastSnapshotGeneration) {
|
||||
RenderStatus renderStatus = candidate.source() == BlockEspSodiumCandidateCollector.CandidateSource.SODIUM_BUFFERED_QUAD
|
||||
? RenderStatus.VISIBLE
|
||||
: RenderStatus.HIDDEN;
|
||||
acceptObservedCandidate(
|
||||
candidate.packedPos(),
|
||||
candidate.state(),
|
||||
candidate.source().name().toLowerCase(),
|
||||
renderStatus
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private void acceptObservedCandidate(long packedPos, BlockState hintedState, String source) {
|
||||
acceptObservedCandidate(packedPos, hintedState, source, RenderStatus.PRESERVE);
|
||||
}
|
||||
|
||||
private void acceptObservedCandidate(long packedPos, BlockState hintedState, String source, RenderStatus renderStatus) {
|
||||
if (mc.level == null || mc.player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
BlockPos pos = BlockPos.of(packedPos);
|
||||
if (!withinDistance(pos)) {
|
||||
removeTargetCandidate(pos);
|
||||
return;
|
||||
}
|
||||
|
||||
BlockState currentState = mc.level.getBlockState(pos);
|
||||
BlockState state = scanContext.isConfiguredTarget(currentState) ? currentState : hintedState;
|
||||
if (!scanContext.isConfiguredTarget(state)) {
|
||||
removeTargetCandidate(pos);
|
||||
return;
|
||||
}
|
||||
|
||||
recordTargetCandidate(pos, state, renderStatus);
|
||||
updateObservedTarget(pos, state, source);
|
||||
}
|
||||
|
||||
private void enqueueReconcile(BlockPos pos) {
|
||||
if (pos == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
long key = pos.asLong();
|
||||
if (queuedReconcileTargets.add(key)) {
|
||||
reconcileQueue.addLast(pos.immutable());
|
||||
}
|
||||
}
|
||||
|
||||
private void maintainReconcileQueue() {
|
||||
if (++fullReconcileRefreshTicks < FULL_RECONCILE_REFRESH_TICKS) {
|
||||
return;
|
||||
}
|
||||
|
||||
fullReconcileRefreshTicks = 0;
|
||||
for (BlockPos pos : observedTargetStates.keySet()) {
|
||||
enqueueReconcile(pos);
|
||||
}
|
||||
}
|
||||
|
||||
private void drainReconcileQueue(String source, int maxEntries) {
|
||||
int processed = 0;
|
||||
while (processed < maxEntries && !reconcileQueue.isEmpty()) {
|
||||
BlockPos pos = reconcileQueue.removeFirst();
|
||||
queuedReconcileTargets.remove(pos.asLong());
|
||||
|
||||
ObservedTarget observed = observedTargetStates.get(pos);
|
||||
if (observed == null) {
|
||||
processed++;
|
||||
continue;
|
||||
}
|
||||
BlockState state = observed.state();
|
||||
|
||||
if (mc.level != null && withinDistance(pos)) {
|
||||
BlockState currentState = mc.level.getBlockState(pos);
|
||||
if (scanContext.isConfiguredTarget(currentState)) {
|
||||
if (currentState != state) {
|
||||
observedTargetStates.put(pos, new ObservedTarget(currentState, observed.rendered()));
|
||||
state = currentState;
|
||||
}
|
||||
} else if (!deobfuscationScanner.hasPositiveState(pos)) {
|
||||
removeTargetCandidate(pos);
|
||||
processed++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
updateObservedTarget(pos, state, source);
|
||||
processed++;
|
||||
}
|
||||
}
|
||||
|
||||
private void maintainSectionScanQueue() {
|
||||
if (mc.level == null || mc.player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int signature = currentScanSignature();
|
||||
if (signature != lastScanSignature || ++sectionScanRefreshTicks >= SECTION_SCAN_REFRESH_TICKS) {
|
||||
lastScanSignature = signature;
|
||||
sectionScanRefreshTicks = 0;
|
||||
requestLocalSectionScan(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void requestLocalSectionScan(boolean burst) {
|
||||
if (mc.level == null || mc.player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int radius = scanBlockRadius();
|
||||
int minY;
|
||||
int maxY;
|
||||
if (limitDistanceValue.get()) {
|
||||
int eyeY = (int) Math.floor(mc.player.getEyePosition().y);
|
||||
minY = eyeY - radius;
|
||||
maxY = eyeY + radius;
|
||||
} else {
|
||||
minY = mc.level.getMinY();
|
||||
maxY = mc.level.getMaxY();
|
||||
}
|
||||
|
||||
sectionScanner.enqueueAround(mc, radius, minY, maxY);
|
||||
if (burst) {
|
||||
sectionScanBurstTicks = SECTION_SCAN_BURST_TICKS;
|
||||
}
|
||||
}
|
||||
|
||||
private void drainSectionScanQueue() {
|
||||
int budget = sectionScanBurstTicks > 0 ? SECTION_SCAN_BURST_BUDGET_PER_TICK : SECTION_SCAN_BUDGET_PER_TICK;
|
||||
if (sectionScanBurstTicks > 0) {
|
||||
sectionScanBurstTicks--;
|
||||
}
|
||||
|
||||
sectionScanner.drain(
|
||||
mc,
|
||||
budget,
|
||||
scanContext::isConfiguredTarget,
|
||||
(pos, state) -> acceptObservedCandidate(pos.asLong(), state, "section")
|
||||
);
|
||||
}
|
||||
|
||||
private void publishFastScanSnapshot() {
|
||||
if (!isEnabled() || mc.player == null) {
|
||||
BlockEspSodiumCandidateCollector.disable();
|
||||
return;
|
||||
}
|
||||
|
||||
BlockEspSodiumCandidateCollector.publishSnapshot(
|
||||
true,
|
||||
scanContext.targetBlocksSnapshot(),
|
||||
limitDistanceValue.get(),
|
||||
scanBlockRadius(),
|
||||
mc.player.getBlockX(),
|
||||
mc.player.getBlockY(),
|
||||
mc.player.getBlockZ(),
|
||||
fastSnapshotGeneration
|
||||
);
|
||||
}
|
||||
|
||||
private int scanBlockRadius() {
|
||||
return limitDistanceValue.get() ? Math.max(1, maxDistanceValue.get()) : 256;
|
||||
}
|
||||
|
||||
private int currentSearchBlocksHash() {
|
||||
Set<String> targets = searchBlocksValue.get();
|
||||
return targets == null ? 0 : targets.hashCode();
|
||||
}
|
||||
|
||||
private int currentScanSignature() {
|
||||
int centerChunkX = SectionPos.blockToSectionCoord(mc.player.getBlockX());
|
||||
int centerChunkZ = SectionPos.blockToSectionCoord(mc.player.getBlockZ());
|
||||
int centerSectionY = SectionPos.blockToSectionCoord(mc.player.getBlockY());
|
||||
int result = 17;
|
||||
result = 31 * result + centerChunkX;
|
||||
result = 31 * result + centerChunkZ;
|
||||
result = 31 * result + centerSectionY;
|
||||
result = 31 * result + (limitDistanceValue.get() ? 1 : 0);
|
||||
result = 31 * result + maxDistanceValue.get();
|
||||
result = 31 * result + currentSearchBlocksHash();
|
||||
return result;
|
||||
}
|
||||
|
||||
private void mergeDeobfuscatedTargets() {
|
||||
for (RenderSectionBlockScanner.ScanResult result : deobfuscationScanner.snapshot(
|
||||
mc,
|
||||
scanContext::isConfiguredTarget,
|
||||
null,
|
||||
maxTargetsValue.get()
|
||||
)) {
|
||||
acceptObservedCandidate(result.pos().asLong(), result.state(), "deobfuscation");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean withinDistance(BlockPos pos) {
|
||||
if (!limitDistanceValue.get() || mc.player == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Vec3 eye = mc.player.getEyePosition();
|
||||
double dx = pos.getX() + 0.5 - eye.x;
|
||||
double dy = pos.getY() + 0.5 - eye.y;
|
||||
double dz = pos.getZ() + 0.5 - eye.z;
|
||||
int maxDistance = maxDistanceValue.get();
|
||||
return dx * dx + dy * dy + dz * dz <= (double) maxDistance * (double) maxDistance;
|
||||
}
|
||||
|
||||
private boolean isCurrentTargetBlock(BlockPos pos) {
|
||||
if (mc.level == null || pos == null) {
|
||||
return false;
|
||||
}
|
||||
return scanContext.isConfiguredTarget(mc.level.getBlockState(pos));
|
||||
}
|
||||
|
||||
private boolean acceptsMode(BlockPos pos, boolean visible) {
|
||||
return switch (modeValue.get()) {
|
||||
case LOS -> visible;
|
||||
case UNRESTRICTED -> true;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public WorldPhase getWorldPhase() {
|
||||
return WorldPhase.END_MAIN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRenderWorld(PoseStack matrices, SubmitNodeCollector consumers) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
|
||||
if (!isEnabled() || mc.level == null || mc.player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
float previousLineWidth = RenderState.lineWidth;
|
||||
RenderState.lineWidth = Math.max(0.5f, lineWidth.get());
|
||||
try {
|
||||
for (Target target : renderTargets.values()) {
|
||||
if (!withinDistance(target.pos)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
BlockState state = target.state;
|
||||
if (mc.level != null) {
|
||||
BlockState current = mc.level.getBlockState(target.pos);
|
||||
if (!scanContext.isConfiguredTarget(current)) {
|
||||
removeTargetCandidate(target.pos);
|
||||
continue;
|
||||
}
|
||||
state = current;
|
||||
if (!state.equals(target.state)) {
|
||||
ObservedTarget observed = observedTargetStates.get(target.pos);
|
||||
boolean rendered = observed != null && observed.rendered();
|
||||
observedTargetStates.put(target.pos, new ObservedTarget(state, rendered));
|
||||
target.state = state;
|
||||
}
|
||||
}
|
||||
|
||||
int color = 0xFF000000 | ((target.visible ? visibleColor.getArgb() : chunkColor.getArgb()) & 0x00FFFFFF);
|
||||
BlockOutlineRenderer.render(renderer, mc.level, target.pos, state, outlineShapeMode.get(), color);
|
||||
}
|
||||
} finally {
|
||||
RenderState.lineWidth = previousLineWidth;
|
||||
}
|
||||
|
||||
renderBlockTracers(renderer, tickDelta);
|
||||
}
|
||||
|
||||
private void drawBox(Renderer3D renderer, AABB box, int r, int g, int b, int a) {
|
||||
double x1 = box.minX;
|
||||
double y1 = box.minY;
|
||||
double z1 = box.minZ;
|
||||
double x2 = box.maxX;
|
||||
double y2 = box.maxY;
|
||||
double z2 = box.maxZ;
|
||||
|
||||
line(renderer, x1, y1, z1, x2, y1, z1, r, g, b, a);
|
||||
line(renderer, x2, y1, z1, x2, y1, z2, r, g, b, a);
|
||||
line(renderer, x2, y1, z2, x1, y1, z2, r, g, b, a);
|
||||
line(renderer, x1, y1, z2, x1, y1, z1, r, g, b, a);
|
||||
|
||||
line(renderer, x1, y2, z1, x2, y2, z1, r, g, b, a);
|
||||
line(renderer, x2, y2, z1, x2, y2, z2, r, g, b, a);
|
||||
line(renderer, x2, y2, z2, x1, y2, z2, r, g, b, a);
|
||||
line(renderer, x1, y2, z2, x1, y2, z1, r, g, b, a);
|
||||
|
||||
line(renderer, x1, y1, z1, x1, y2, z1, r, g, b, a);
|
||||
line(renderer, x2, y1, z1, x2, y2, z1, r, g, b, a);
|
||||
line(renderer, x2, y1, z2, x2, y2, z2, r, g, b, a);
|
||||
line(renderer, x1, y1, z2, x1, y2, z2, r, g, b, a);
|
||||
}
|
||||
|
||||
private void line(Renderer3D renderer,
|
||||
double x1, double y1, double z1,
|
||||
double x2, double y2, double z2,
|
||||
int r, int g, int b, int a) {
|
||||
renderer.line(x1, y1, z1, x2, y2, z2, r, g, b, a);
|
||||
}
|
||||
|
||||
private void renderBlockTracers(Renderer3D renderer, float tickDelta) {
|
||||
Tracers tracers = Modules.get(Tracers.class);
|
||||
if (!useTracersValue.get() || tracers == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!clusterTracersValue.get()) {
|
||||
for (Target target : renderTargets.values()) {
|
||||
if (!withinDistance(target.pos) || !isCurrentTargetBlock(target.pos)) {
|
||||
continue;
|
||||
}
|
||||
int color = target.visible ? visibleColor.getArgb() : chunkColor.getArgb();
|
||||
tracers.render3DTracersRaw(renderer, tickDelta, List.of(Vec3.atCenterOf(target.pos)), color);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
List<List<Target>> clusters = buildClusters(renderTargets.values().stream()
|
||||
.filter(target -> withinDistance(target.pos) && isCurrentTargetBlock(target.pos))
|
||||
.toList());
|
||||
for (List<Target> cluster : clusters) {
|
||||
boolean anyVisible = cluster.stream().anyMatch(target -> target.visible);
|
||||
int color = anyVisible ? visibleColor.getArgb() : chunkColor.getArgb();
|
||||
tracers.render3DTracersRaw(renderer, tickDelta, List.of(clusterCenter(cluster)), color);
|
||||
}
|
||||
}
|
||||
|
||||
private List<List<Target>> buildClusters(Collection<Target> targets) {
|
||||
Map<BlockPos, Target> map = new HashMap<>();
|
||||
for (Target target : targets) {
|
||||
map.put(target.pos, target);
|
||||
}
|
||||
|
||||
Set<BlockPos> visited = new HashSet<>();
|
||||
List<List<Target>> clusters = new ArrayList<>();
|
||||
|
||||
for (Target target : targets) {
|
||||
if (visited.contains(target.pos)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<Target> cluster = new ArrayList<>();
|
||||
ArrayDeque<BlockPos> queue = new ArrayDeque<>();
|
||||
|
||||
queue.add(target.pos);
|
||||
visited.add(target.pos);
|
||||
|
||||
while (!queue.isEmpty()) {
|
||||
BlockPos current = queue.poll();
|
||||
Target currentTarget = map.get(current);
|
||||
if (currentTarget != null) {
|
||||
cluster.add(currentTarget);
|
||||
}
|
||||
|
||||
for (BlockPos neighbor : new BlockPos[]{
|
||||
current.above(), current.below(),
|
||||
current.north(), current.south(),
|
||||
current.east(), current.west()
|
||||
}) {
|
||||
if (!visited.contains(neighbor) && map.containsKey(neighbor)) {
|
||||
visited.add(neighbor);
|
||||
queue.add(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clusters.add(cluster);
|
||||
}
|
||||
|
||||
return clusters;
|
||||
}
|
||||
|
||||
private Vec3 clusterCenter(List<Target> cluster) {
|
||||
double x = 0.0;
|
||||
double y = 0.0;
|
||||
double z = 0.0;
|
||||
for (Target target : cluster) {
|
||||
Vec3 center = Vec3.atCenterOf(target.pos);
|
||||
x += center.x;
|
||||
y += center.y;
|
||||
z += center.z;
|
||||
}
|
||||
int size = cluster.size();
|
||||
return new Vec3(x / size, y / size, z / size);
|
||||
}
|
||||
|
||||
public static class Target {
|
||||
BlockPos pos;
|
||||
BlockState state;
|
||||
AABB box;
|
||||
boolean visible;
|
||||
}
|
||||
|
||||
private enum RenderStatus {
|
||||
PRESERVE,
|
||||
HIDDEN,
|
||||
VISIBLE
|
||||
}
|
||||
|
||||
private record ObservedTarget(BlockState state, boolean rendered) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,312 @@
|
|||
/*
|
||||
* 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.module.modules.visuals;
|
||||
|
||||
import dev.loki.lovisual.config.common.CommonSettingSchemas;
|
||||
import dev.loki.lovisual.config.values.BooleanValue;
|
||||
import dev.loki.lovisual.config.values.EnumValue;
|
||||
import dev.loki.lovisual.config.values.NumberValue;
|
||||
import dev.loki.lovisual.config.values.RGBAColorValue;
|
||||
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.render.engine.animation.AnimatedRenderColors;
|
||||
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.render.helpers.HeadAnchor;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
|
||||
/**
|
||||
* Classic conical Chinese hat (rice hat / coolie hat): a wide flat brim annulus
|
||||
* topped by a cone dome. Geometry lives in {@link ChinaHatModel}, colors in
|
||||
* {@link ChinaHatPalette}; this class only wires settings to rendering.
|
||||
*/
|
||||
@ModuleInfo(
|
||||
id = "chinahat",
|
||||
displayName = "ChinaHat",
|
||||
category = ModuleCategory.VISUALS
|
||||
)
|
||||
public final class ChinaHat extends Module {
|
||||
|
||||
private final Minecraft mc = Minecraft.getInstance();
|
||||
|
||||
private final EnumValue<RenderTarget> renderTarget =
|
||||
enumMode(
|
||||
"render_target",
|
||||
RenderTarget.BOTH,
|
||||
RenderTarget.values()
|
||||
);
|
||||
private final BooleanValue useRelations =
|
||||
boolCommon(
|
||||
"use_relations",
|
||||
"use_relations",
|
||||
CommonSettingSchemas.PLAYER_USE_RELATIONS,
|
||||
true
|
||||
);
|
||||
private final RGBAColorValue primaryColor =
|
||||
visibleWhen(common(
|
||||
color(
|
||||
"primary_color",
|
||||
"#CCE8C547"
|
||||
),
|
||||
CommonSettingSchemas.RENDER_PRIMARY_COLOR.commonI18nKey()
|
||||
), () -> !useRelations.get());
|
||||
private final EnumValue<ColorMode> colorMode =
|
||||
enumCommon(
|
||||
"color_mode",
|
||||
"color_mode",
|
||||
CommonSettingSchemas.RENDER_COLOR_MODE,
|
||||
ColorMode.STATIC,
|
||||
ColorMode.values()
|
||||
);
|
||||
private final RGBAColorValue secondaryColor =
|
||||
visibleWhen(common(
|
||||
color(
|
||||
"secondary_color",
|
||||
"#AA9A7A1A"
|
||||
),
|
||||
CommonSettingSchemas.RENDER_SECONDARY_COLOR.commonI18nKey()
|
||||
), this::usesSecondaryManualColor);
|
||||
private final NumberValue<Integer> colorSpeed =
|
||||
numCommon(
|
||||
"color_speed",
|
||||
"color_speed",
|
||||
CommonSettingSchemas.RENDER_COLOR_SPEED,
|
||||
18,
|
||||
2,
|
||||
54
|
||||
);
|
||||
private final NumberValue<Float> scale =
|
||||
numCommon(
|
||||
"scale",
|
||||
"scale",
|
||||
CommonSettingSchemas.RENDER_SCALE,
|
||||
1.15f,
|
||||
0.45f,
|
||||
2.0f
|
||||
);
|
||||
private final BooleanValue depthTest =
|
||||
boolCommon(
|
||||
"depth_test",
|
||||
"depth_test",
|
||||
CommonSettingSchemas.RENDER_DEPTH_TEST,
|
||||
true
|
||||
);
|
||||
private final RGBAColorValue rimColor =
|
||||
visibleWhen(color(
|
||||
"rim_color",
|
||||
"#EEF3D18A"
|
||||
), () -> !useRelations.get());
|
||||
private final NumberValue<Integer> segments =
|
||||
num(
|
||||
"segments",
|
||||
40,
|
||||
6,
|
||||
96
|
||||
);
|
||||
private final NumberValue<Float> brimRadius =
|
||||
num(
|
||||
"brim_radius",
|
||||
0.45f,
|
||||
0.2f,
|
||||
0.7f
|
||||
);
|
||||
private final NumberValue<Float> domeRadius =
|
||||
num(
|
||||
"dome_radius",
|
||||
0.16f,
|
||||
0.06f,
|
||||
0.35f
|
||||
);
|
||||
private final NumberValue<Float> domeHeight =
|
||||
num(
|
||||
"dome_height",
|
||||
0.3f,
|
||||
0.1f,
|
||||
0.55f
|
||||
);
|
||||
private final NumberValue<Float> brimThickness =
|
||||
num(
|
||||
"brim_thickness",
|
||||
0.025f,
|
||||
0.01f,
|
||||
0.08f
|
||||
);
|
||||
private final NumberValue<Float> headTopOffset =
|
||||
num(
|
||||
"head_top_offset",
|
||||
0.3f,
|
||||
0.05f,
|
||||
0.5f
|
||||
);
|
||||
private final NumberValue<Float> headForwardOffset =
|
||||
num(
|
||||
"head_forward_offset",
|
||||
0.0f,
|
||||
-0.25f,
|
||||
0.25f
|
||||
);
|
||||
private final NumberValue<Float> headRightOffset =
|
||||
num(
|
||||
"head_right_offset",
|
||||
0.0f,
|
||||
-0.25f,
|
||||
0.25f
|
||||
);
|
||||
|
||||
@Override
|
||||
public WorldPhase getWorldPhase() {
|
||||
return WorldPhase.BEFORE_TRANSLUCENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
|
||||
if (!isEnabled() || mc.level == null || mc.player == null) return;
|
||||
|
||||
MeshBuilder tris = renderer.batch(
|
||||
depthTest.get()
|
||||
? LoVisualRenderPipelines.WORLD_COLORED_DEPTH
|
||||
: LoVisualRenderPipelines.WORLD_COLORED,
|
||||
depthTest.get()
|
||||
? Renderer3D.DepthMode.PRE_DEPTH
|
||||
: Renderer3D.DepthMode.NONE
|
||||
);
|
||||
|
||||
MeshBuilder lines = renderer.batch(
|
||||
depthTest.get()
|
||||
? LoVisualRenderPipelines.WORLD_COLORED_LINES_DEPTH
|
||||
: LoVisualRenderPipelines.WORLD_COLORED_LINES,
|
||||
depthTest.get()
|
||||
? Renderer3D.DepthMode.PRE_DEPTH
|
||||
: Renderer3D.DepthMode.NONE
|
||||
);
|
||||
|
||||
if (tris == null || lines == null) return;
|
||||
|
||||
if (shouldRenderSelf() && !mc.options.getCameraType().isFirstPerson()) {
|
||||
renderAttached(tris, lines, mc.player, tickDelta);
|
||||
}
|
||||
|
||||
if (shouldRenderPlayers()) {
|
||||
for (Player player : mc.level.players()) {
|
||||
if (player == null || player == mc.player || player.isRemoved()) continue;
|
||||
if (!player.isAlive() || player.getHealth() <= 0.0f) continue;
|
||||
|
||||
renderAttached(tris, lines, player, tickDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldRenderSelf() {
|
||||
return renderTarget.get() == RenderTarget.SELF || renderTarget.get() == RenderTarget.BOTH;
|
||||
}
|
||||
|
||||
private boolean shouldRenderPlayers() {
|
||||
return renderTarget.get() == RenderTarget.PLAYERS || renderTarget.get() == RenderTarget.BOTH;
|
||||
}
|
||||
|
||||
private boolean usesSecondaryManualColor() {
|
||||
if (useRelations.get()) return false;
|
||||
return AnimatedRenderColors.usesSecondary(animatedColorMode());
|
||||
}
|
||||
|
||||
private void renderAttached(MeshBuilder tris, MeshBuilder lines, Player player, float tickDelta) {
|
||||
HeadAnchor anchor = HeadAnchor.of(
|
||||
player,
|
||||
tickDelta,
|
||||
headTopOffset.get(),
|
||||
headForwardOffset.get(),
|
||||
headRightOffset.get()
|
||||
);
|
||||
|
||||
ChinaHatPalette.Palette palette = ChinaHatPalette.resolve(
|
||||
player,
|
||||
useRelations.get(),
|
||||
primaryColor.getArgb(),
|
||||
secondaryColor.getArgb(),
|
||||
rimColor.getArgb()
|
||||
);
|
||||
|
||||
AnimatedRenderColors.Mode mode = animatedColorMode();
|
||||
int speed = colorSpeed.get();
|
||||
float size = scale.get();
|
||||
|
||||
ChinaHatModel.build(
|
||||
tris,
|
||||
lines,
|
||||
anchor,
|
||||
segments.get(),
|
||||
brimRadius.get() * size,
|
||||
domeRadius.get() * size,
|
||||
domeHeight.get() * size,
|
||||
brimThickness.get() * size,
|
||||
0.0f,
|
||||
0.0f,
|
||||
0.0f,
|
||||
(index, light, alphaMul) -> ChinaHatPalette.colorAt(palette, mode, speed, index, light, alphaMul),
|
||||
(index, light, alphaMul) -> ChinaHatPalette.colorAt(palette, mode, speed, index, light, alphaMul),
|
||||
(index, light, alphaMul) -> ChinaHatPalette.rimColorAt(palette, mode, speed, index, alphaMul),
|
||||
1.0f
|
||||
);
|
||||
}
|
||||
|
||||
private AnimatedRenderColors.Mode animatedColorMode() {
|
||||
return switch (colorMode.get()) {
|
||||
case RAINBOW -> AnimatedRenderColors.Mode.RAINBOW;
|
||||
case LIGHT_RAINBOW -> AnimatedRenderColors.Mode.LIGHT_RAINBOW;
|
||||
case SKY -> AnimatedRenderColors.Mode.SKY;
|
||||
case FADE -> AnimatedRenderColors.Mode.FADE;
|
||||
case DOUBLE_COLOR -> AnimatedRenderColors.Mode.DOUBLE_COLOR;
|
||||
case ANALOGOUS -> AnimatedRenderColors.Mode.ANALOGOUS;
|
||||
case THEME -> AnimatedRenderColors.Mode.THEME;
|
||||
case STATIC -> AnimatedRenderColors.Mode.STATIC;
|
||||
};
|
||||
}
|
||||
|
||||
public enum RenderTarget implements EnumValue.IdProvider {
|
||||
SELF("self"),
|
||||
PLAYERS("players"),
|
||||
BOTH("both");
|
||||
|
||||
private final String id;
|
||||
|
||||
RenderTarget(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String id() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
public enum ColorMode implements EnumValue.IdProvider {
|
||||
STATIC("static"),
|
||||
RAINBOW("rainbow"),
|
||||
LIGHT_RAINBOW("light_rainbow"),
|
||||
SKY("sky"),
|
||||
FADE("fade"),
|
||||
DOUBLE_COLOR("double_color"),
|
||||
ANALOGOUS("analogous"),
|
||||
THEME("theme");
|
||||
|
||||
private final String id;
|
||||
|
||||
ColorMode(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String id() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* 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.module.modules.visuals;
|
||||
|
||||
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||
import dev.loki.lovisual.render.helpers.HeadAnchor;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/**
|
||||
* Geometry for the classic ChinaHat: a wide flat brim (annulus) plus a conical dome.
|
||||
* Pure mesh assembly, no Minecraft state. Per-vertex colors come from the caller.
|
||||
*/
|
||||
final class ChinaHatModel {
|
||||
private ChinaHatModel() {
|
||||
}
|
||||
|
||||
interface Shade {
|
||||
int color(int index, float verticalLight, float alphaMul);
|
||||
}
|
||||
|
||||
static void build(MeshBuilder tris,
|
||||
MeshBuilder lines,
|
||||
HeadAnchor anchor,
|
||||
int segments,
|
||||
float brimRadius,
|
||||
float domeRadius,
|
||||
float domeHeight,
|
||||
float brimThickness,
|
||||
float yawDeg,
|
||||
float pitchDeg,
|
||||
float rollDeg,
|
||||
Shade domeShade,
|
||||
Shade brimShade,
|
||||
Shade rimShade,
|
||||
float alphaMul) {
|
||||
if (tris == null || lines == null || segments < 3) return;
|
||||
|
||||
float outerY = brimThickness;
|
||||
float innerY = 0.0f;
|
||||
Vec3[] outerTop = new Vec3[segments];
|
||||
Vec3[] outerBottom = new Vec3[segments];
|
||||
Vec3[] innerTop = new Vec3[segments];
|
||||
Vec3[] innerBottom = new Vec3[segments];
|
||||
|
||||
for (int i = 0; i < segments; i++) {
|
||||
double angle = (Math.PI * 2.0 * i) / segments;
|
||||
float s = Mth.sin((float) angle);
|
||||
float c = Mth.cos((float) angle);
|
||||
outerTop[i] = anchor.localRotated(s * brimRadius, outerY, c * brimRadius, yawDeg, pitchDeg, rollDeg);
|
||||
outerBottom[i] = anchor.localRotated(s * brimRadius, innerY, c * brimRadius, yawDeg, pitchDeg, rollDeg);
|
||||
innerTop[i] = anchor.localRotated(s * domeRadius, outerY, c * domeRadius, yawDeg, pitchDeg, rollDeg);
|
||||
innerBottom[i] = anchor.localRotated(s * domeRadius, innerY, c * domeRadius, yawDeg, pitchDeg, rollDeg);
|
||||
}
|
||||
|
||||
Vec3 tip = anchor.localRotated(0.0f, outerY + domeHeight, 0.0f, yawDeg, pitchDeg, rollDeg);
|
||||
|
||||
for (int i = 0; i < segments; i++) {
|
||||
int next = (i + 1) % segments;
|
||||
|
||||
int domeA = domeShade.color(i, 1.06f, alphaMul);
|
||||
int domeB = domeShade.color(next, 1.02f, alphaMul);
|
||||
triangle(tris, tip, innerTop[i], domeA, innerTop[next], domeB);
|
||||
|
||||
int brimTopA = brimShade.color(i, 0.94f, alphaMul);
|
||||
int brimTopB = brimShade.color(next, 0.90f, alphaMul);
|
||||
quad(tris,
|
||||
innerTop[i], brimTopA,
|
||||
innerTop[next], brimTopB,
|
||||
outerTop[next], brimTopB,
|
||||
outerTop[i], brimTopA);
|
||||
|
||||
int brimBotA = brimShade.color(i, 0.42f, alphaMul);
|
||||
int brimBotB = brimShade.color(next, 0.40f, alphaMul);
|
||||
quad(tris,
|
||||
outerBottom[i], brimBotA,
|
||||
outerBottom[next], brimBotB,
|
||||
innerBottom[next], brimBotA,
|
||||
innerBottom[i], brimBotB);
|
||||
|
||||
int edgeA = brimShade.color(i, 0.72f, alphaMul);
|
||||
int edgeB = brimShade.color(next, 0.70f, alphaMul);
|
||||
quad(tris,
|
||||
outerBottom[i], edgeA,
|
||||
outerBottom[next], edgeB,
|
||||
outerTop[next], edgeB,
|
||||
outerTop[i], edgeA);
|
||||
|
||||
int rim1 = rimShade.color(i, 1.0f, alphaMul);
|
||||
int rim2 = rimShade.color(next, 1.0f, alphaMul);
|
||||
lineGradient(lines, outerTop[i], outerTop[next], rim1, rim2);
|
||||
}
|
||||
}
|
||||
|
||||
private static void triangle(MeshBuilder mesh, Vec3 a, Vec3 b, int cA, Vec3 c, int cC) {
|
||||
mesh.ensureTriCapacity();
|
||||
int i1 = putVertex(mesh, a, cA);
|
||||
int i2 = putVertex(mesh, b, cA);
|
||||
int i3 = putVertex(mesh, c, cC);
|
||||
mesh.triangle(i1, i2, i3);
|
||||
}
|
||||
|
||||
private static void quad(MeshBuilder mesh,
|
||||
Vec3 p1, int c1,
|
||||
Vec3 p2, int c2,
|
||||
Vec3 p3, int c3,
|
||||
Vec3 p4, int c4) {
|
||||
mesh.ensureQuadCapacity();
|
||||
int i1 = putVertex(mesh, p1, c1);
|
||||
int i2 = putVertex(mesh, p2, c2);
|
||||
int i3 = putVertex(mesh, p3, c3);
|
||||
int i4 = putVertex(mesh, p4, c4);
|
||||
mesh.quad(i1, i2, i3, i4);
|
||||
}
|
||||
|
||||
private static void lineGradient(MeshBuilder mesh, Vec3 a, Vec3 b, int cA, int cB) {
|
||||
mesh.ensureLineCapacity();
|
||||
int i1 = putVertex(mesh, a, cA);
|
||||
int i2 = putVertex(mesh, b, cB);
|
||||
mesh.line(i1, i2);
|
||||
}
|
||||
|
||||
private static int putVertex(MeshBuilder mesh, Vec3 p, int argb) {
|
||||
return mesh.vec3(p.x, p.y, p.z)
|
||||
.color((argb >>> 24) & 255, (argb >>> 16) & 255, (argb >>> 8) & 255, argb & 255)
|
||||
.next();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* 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.module.modules.visuals;
|
||||
|
||||
import dev.loki.lovisual.features.relations.CategoryService;
|
||||
import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors;
|
||||
import dev.loki.lovisual.render.engine.animation.AnimationUtility;
|
||||
import dev.loki.lovisual.util.time.GameClock;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
|
||||
/**
|
||||
* Color resolution helpers for ChinaHat. Shared color-math and per-vertex shading,
|
||||
* mirroring the TazikHat palette pipeline but reusable by the facade module.
|
||||
*/
|
||||
final class ChinaHatPalette {
|
||||
private ChinaHatPalette() {
|
||||
}
|
||||
|
||||
private static final double RAINBOW_SPATIAL_SCALE = 12.0;
|
||||
|
||||
record Palette(int primary, int secondary, int rim) {
|
||||
}
|
||||
|
||||
static Palette resolve(Player player,
|
||||
boolean useRelations,
|
||||
int primaryArgb,
|
||||
int secondaryArgb,
|
||||
int rimArgb) {
|
||||
if (useRelations) {
|
||||
int relation = CategoryService.getColor(player);
|
||||
int primary = withAlpha(relation, alphaOr(primaryArgb, 255));
|
||||
int secondary = withAlpha(
|
||||
AnimatedRenderColors.analogousColor(relation),
|
||||
alphaOr(secondaryArgb, 170)
|
||||
);
|
||||
int rim = withAlpha(brighten(relation, 1.35f), alphaOr(rimArgb, 238));
|
||||
|
||||
return new Palette(primary, secondary, rim);
|
||||
}
|
||||
|
||||
return new Palette(primaryArgb, secondaryArgb, rimArgb);
|
||||
}
|
||||
|
||||
static int colorAt(Palette palette,
|
||||
AnimatedRenderColors.Mode mode,
|
||||
int speed,
|
||||
int index,
|
||||
float verticalLight,
|
||||
float alphaMul) {
|
||||
int dynamic = getDynamicColor(palette.primary, palette.secondary, mode, speed, index);
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
static int rimColorAt(Palette palette,
|
||||
AnimatedRenderColors.Mode mode,
|
||||
int speed,
|
||||
int index,
|
||||
float alphaMul) {
|
||||
int dynamic = getDynamicColor(palette.rim, palette.secondary, mode, speed, index + 120);
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
static int getDynamicColor(int primary,
|
||||
int secondary,
|
||||
AnimatedRenderColors.Mode mode,
|
||||
int speed,
|
||||
int index) {
|
||||
int spatialIndex = Math.floorMod((int) Math.round(index * RAINBOW_SPATIAL_SCALE), 3600);
|
||||
return AnimatedRenderColors.resolve(mode, speed, spatialIndex, primary, secondary, true);
|
||||
}
|
||||
|
||||
static int mix(int a, int b, float t) {
|
||||
float clamped = AnimationUtility.clamp01(t);
|
||||
|
||||
int aa = alpha(a);
|
||||
int ar = red(a);
|
||||
int ag = green(a);
|
||||
int ab = blue(a);
|
||||
|
||||
int ba = alpha(b);
|
||||
int br = red(b);
|
||||
int bg = green(b);
|
||||
int bb = blue(b);
|
||||
|
||||
int ca = Math.round(AnimationUtility.lerp(aa, ba, clamped));
|
||||
int cr = Math.round(AnimationUtility.lerp(ar, br, clamped));
|
||||
int cg = Math.round(AnimationUtility.lerp(ag, bg, clamped));
|
||||
int cb = Math.round(AnimationUtility.lerp(ab, bb, clamped));
|
||||
|
||||
return (ca << 24) | (cr << 16) | (cg << 8) | cb;
|
||||
}
|
||||
|
||||
static int multiplyRgb(int argb, float factor) {
|
||||
int a = alpha(argb);
|
||||
int r = Mth.clamp(Math.round(red(argb) * factor), 0, 255);
|
||||
int g = Mth.clamp(Math.round(green(argb) * factor), 0, 255);
|
||||
int b = Mth.clamp(Math.round(blue(argb) * factor), 0, 255);
|
||||
return (a << 24) | (r << 16) | (g << 8) | b;
|
||||
}
|
||||
|
||||
static int brighten(int argb, float factor) {
|
||||
return multiplyRgb(argb, factor);
|
||||
}
|
||||
|
||||
static int withAlpha(int argb, int alpha) {
|
||||
int a = Mth.clamp(alpha, 0, 255);
|
||||
return (a << 24) | (argb & 0x00FFFFFF);
|
||||
}
|
||||
|
||||
static int alphaOr(int argb, int fallback) {
|
||||
int alpha = alpha(argb);
|
||||
return alpha <= 0 ? Mth.clamp(fallback, 0, 255) : alpha;
|
||||
}
|
||||
|
||||
static int red(int argb) {
|
||||
return (argb >>> 16) & 255;
|
||||
}
|
||||
|
||||
static int green(int argb) {
|
||||
return (argb >>> 8) & 255;
|
||||
}
|
||||
|
||||
static int blue(int argb) {
|
||||
return argb & 255;
|
||||
}
|
||||
|
||||
static int alpha(int argb) {
|
||||
return (argb >>> 24) & 255;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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.module.modules.visuals;
|
||||
|
||||
import dev.loki.lovisual.config.values.NumberValue;
|
||||
import dev.loki.lovisual.config.values.RGBAColorValue;
|
||||
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.render.engine.RenderState;
|
||||
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 net.minecraft.client.Minecraft;
|
||||
import net.minecraft.world.effect.MobEffects;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
|
||||
@ModuleInfo(
|
||||
id = "invisesp",
|
||||
displayName = "InvisESP",
|
||||
aliases = {"invis"},
|
||||
category = ModuleCategory.VISUALS,
|
||||
description = "module.invisesp.description"
|
||||
)
|
||||
public final class InvisEsp extends Module {
|
||||
|
||||
private final Minecraft mc = Minecraft.getInstance();
|
||||
|
||||
private final RGBAColorValue outlineColor =
|
||||
color("invisesp_color", "color", "#FF00E5FF");
|
||||
private final NumberValue<Float> lineWidth =
|
||||
num("invisesp_line_width", "line_width", 1.5f, 0.5f, 6.0f);
|
||||
|
||||
{
|
||||
setDefaultBind("L");
|
||||
}
|
||||
|
||||
@Override
|
||||
public WorldPhase getWorldPhase() {
|
||||
return WorldPhase.END_MAIN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
|
||||
if (!isEnabled() || mc.level == null || mc.player == null) return;
|
||||
|
||||
float previousLineWidth = RenderState.lineWidth;
|
||||
MeshBuilder lines;
|
||||
try {
|
||||
RenderState.lineWidth = lineWidth.get();
|
||||
lines = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_LINES, Renderer3D.DepthMode.NONE);
|
||||
} finally {
|
||||
RenderState.lineWidth = previousLineWidth;
|
||||
}
|
||||
|
||||
int color = outlineColor.getArgb();
|
||||
for (Player player : mc.level.players()) {
|
||||
if (player == mc.player) continue;
|
||||
if (!isInvisible(player)) continue;
|
||||
|
||||
AABB box = Esp2DRenderer.resolveWorldBox(player, tickDelta);
|
||||
EspMeshBuilder.addOutlineBox(lines, box, color, color, color, color, color, color, color, color);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInvisible(Player player) {
|
||||
return player.isInvisible() || player.hasEffect(MobEffects.INVISIBILITY);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +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.module.modules.visuals;
|
||||
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
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 = "seeinvisibles",
|
||||
displayName = "SeeInvisibles",
|
||||
category = ModuleCategory.VISUALS
|
||||
)
|
||||
public class SeeInvisibles extends Module {
|
||||
|
||||
private final NumberValue<Integer> alpha = num("seeInvisiblesAlpha", "alpha", 110, 0, 255);
|
||||
|
||||
public boolean shouldRenderInvisiblePlayer(LivingEntity entity) {
|
||||
return isEnabled() && entity instanceof Player && entity.isInvisible();
|
||||
}
|
||||
|
||||
public boolean shouldRenderInvisiblePlayer(EntityType<?> entityType, boolean invisible) {
|
||||
return isEnabled() && invisible && entityType == net.minecraft.world.entity.EntityTypes.PLAYER;
|
||||
}
|
||||
|
||||
public int getInvisiblePlayerTintArgb() {
|
||||
int alphaByte = Mth.clamp(alpha.get(), 0, 255);
|
||||
return (alphaByte << 24) | 0x00FFFFFF;
|
||||
}
|
||||
|
||||
public float getInvisiblePlayerAlpha01() {
|
||||
return Mth.clamp(alpha.get(), 0, 255) / 255.0f;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,356 +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.module.modules.visuals;
|
||||
|
||||
import com.mojang.blaze3d.textures.GpuSampler;
|
||||
import com.mojang.blaze3d.textures.GpuTextureView;
|
||||
import dev.loki.lovisual.features.module.*;
|
||||
import dev.loki.lovisual.features.module.Module;
|
||||
import dev.loki.lovisual.util.screen.ClientScreen;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.screens.inventory.InventoryScreen;
|
||||
import net.minecraft.client.renderer.texture.AbstractTexture;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.phys.Vec2;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.joml.Matrix4f;
|
||||
import dev.loki.lovisual.config.values.BooleanValue;
|
||||
import dev.loki.lovisual.config.values.ModeValue;
|
||||
import dev.loki.lovisual.features.module.*;
|
||||
import dev.loki.lovisual.features.relations.CategoryService;
|
||||
import dev.loki.lovisual.mixininterface.IEntity;
|
||||
import dev.loki.lovisual.render.engine.animation.AnimationUtility;
|
||||
import dev.loki.lovisual.render.engine.color.RenderColor;
|
||||
import dev.loki.lovisual.render.engine.core.ViewportContext;
|
||||
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||
import dev.loki.lovisual.render.engine.renderer.MeshRenderer;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
|
||||
import dev.loki.lovisual.render.engine.text.TextRenderer;
|
||||
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
//todo Description
|
||||
@ModuleInfo(id = "tracers", displayName = "Tracers", aliases = {"arrows", "lines"}, category = ModuleCategory.VISUALS)
|
||||
public class Tracers extends Module {
|
||||
|
||||
private static final String SETTING_TRACE_FRIENDS = "trace_friends";
|
||||
private static final String SETTING_MOVE_RADIUS_BONUS = "move_radius_bonus";
|
||||
private static final String SETTING_MODE = "mode";
|
||||
private static final String SETTING_ARROW_TEXTURE = "arrow_texture";
|
||||
private static final Identifier ARROW_1 = Identifier.fromNamespaceAndPath("lovisual", "textures/hud/elements/arrow.png");
|
||||
private static final Identifier ARROW_2 = Identifier.fromNamespaceAndPath("lovisual", "textures/hud/elements/arrow2.png");
|
||||
private static final Identifier ARROW_3 = Identifier.fromNamespaceAndPath("lovisual", "textures/hud/elements/arrow3.png");
|
||||
private static final Identifier ARROW_4 = Identifier.fromNamespaceAndPath("lovisual", "textures/hud/elements/arrow4.png");
|
||||
private static final float BASE_RADIUS = 48.0f;
|
||||
private static final float INVENTORY_RADIUS_BONUS = 50.0f;
|
||||
private static final float MOVE_RADIUS_BONUS = 1.5f;
|
||||
private static final float ARROW_W = 10.0f;
|
||||
private static final float ARROW_H = 10.0f;
|
||||
private static final float ARROW_OFFSET_X = -5.0f;
|
||||
private static final float ARROW_OFFSET_Y = -6.0f;
|
||||
private static final int ARROW_ALPHA = 235;
|
||||
private static final float ARROW_ROTATION_OFFSET_DEG = 90.0f;
|
||||
private static final float ARROW_DISTANCE_NEAR = 6.0f;
|
||||
private static final float ARROW_DISTANCE_FAR = 60.0f;
|
||||
private static final float ARROW_RADIUS_MIN_FACTOR = 0.55f;
|
||||
private static final float ARROW_RADIUS_MAX_FACTOR = 1.0f;
|
||||
private static final MeshBuilder ARROW_MESH =
|
||||
new MeshBuilder(LoVisualRenderPipelines.UI_TEXTURED_ADDITIVE);
|
||||
private final Minecraft mc = Minecraft.getInstance();
|
||||
private final BooleanValue traceFriendsValue = bool("tracersTraceFriends", SETTING_TRACE_FRIENDS, false);
|
||||
private final ModeValue tracersMode =
|
||||
modeSetting("tracersMode", SETTING_MODE, "Lines", "Lines", "Arrows");
|
||||
private final BooleanValue moveRadiusBonusValue =
|
||||
visibleWhen(bool("tracersMoveRadiusBonus", SETTING_MOVE_RADIUS_BONUS, false), this::isArrowsMode);
|
||||
private final ModeValue arrowTexture =
|
||||
visibleWhen(modeSetting("tracersArrowTexture", SETTING_ARROW_TEXTURE, "Arrow", "Arrow", "Arrow2", "Arrow3", "Arrow4"),
|
||||
this::isArrowsMode);
|
||||
private float animationStep;
|
||||
private float animatedYaw;
|
||||
private float animatedPitch;
|
||||
private float yaw;
|
||||
|
||||
private static boolean isMoving(Vec2 move) {
|
||||
if (move == null) return false;
|
||||
return move.x != 0.0f || move.y != 0.0f;
|
||||
}
|
||||
|
||||
private static boolean isNameValid(String name) {
|
||||
if (name == null) return false;
|
||||
int len = name.length();
|
||||
if (len < 3 || len > 16) return false;
|
||||
for (int i = 0; i < len; i++) {
|
||||
char c = name.charAt(i);
|
||||
if (c == '_') continue;
|
||||
if (!Character.isLetterOrDigit(c)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int withAlpha(int argb, int alpha) {
|
||||
int a = Mth.clamp(alpha, 0, 255);
|
||||
return (argb & 0x00FFFFFF) | (a << 24);
|
||||
}
|
||||
|
||||
private static float radiusFactorForDistance(double distance) {
|
||||
if (distance <= ARROW_DISTANCE_NEAR) return ARROW_RADIUS_MIN_FACTOR;
|
||||
if (distance >= ARROW_DISTANCE_FAR) return ARROW_RADIUS_MAX_FACTOR;
|
||||
float t = (float) ((distance - ARROW_DISTANCE_NEAR) / (ARROW_DISTANCE_FAR - ARROW_DISTANCE_NEAR));
|
||||
t = Mth.clamp(t, 0.0f, 1.0f);
|
||||
return Mth.lerp(t, ARROW_RADIUS_MIN_FACTOR, ARROW_RADIUS_MAX_FACTOR);
|
||||
}
|
||||
|
||||
private static void renderArrow(Identifier textureId,
|
||||
double x,
|
||||
double y,
|
||||
float angleRad,
|
||||
int argb,
|
||||
float scale) {
|
||||
if (textureId == null) return;
|
||||
Renderer2D.deferRenderThreadAction(() -> renderArrowImmediate(textureId, x, y, angleRad, argb, scale));
|
||||
}
|
||||
|
||||
private static void renderArrowImmediate(Identifier textureId,
|
||||
double x,
|
||||
double y,
|
||||
float angleRad,
|
||||
int argb,
|
||||
float scale) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (mc == null || mc.gameRenderer == null || mc.gameRenderer.mainRenderTarget() == null) return;
|
||||
AbstractTexture tex = mc.getTextureManager().getTexture(textureId);
|
||||
if (tex == null) return;
|
||||
GpuTextureView view = tex.getTextureView();
|
||||
GpuSampler sampler = tex.getSampler();
|
||||
if (view == null || sampler == null) return;
|
||||
|
||||
RenderColor c = new RenderColor(argb);
|
||||
ARROW_MESH.begin();
|
||||
ARROW_MESH.ensureQuadCapacity();
|
||||
int i1 = ARROW_MESH.vec2(ARROW_OFFSET_X * scale, ARROW_OFFSET_Y * scale)
|
||||
.vec2(0, 0).color(c.r, c.g, c.b, c.a).next();
|
||||
int i2 = ARROW_MESH.vec2(ARROW_OFFSET_X * scale, (ARROW_OFFSET_Y + ARROW_H) * scale)
|
||||
.vec2(0, 1).color(c.r, c.g, c.b, c.a).next();
|
||||
int i3 = ARROW_MESH.vec2((ARROW_OFFSET_X + ARROW_W) * scale, (ARROW_OFFSET_Y + ARROW_H) * scale)
|
||||
.vec2(1, 1).color(c.r, c.g, c.b, c.a).next();
|
||||
int i4 = ARROW_MESH.vec2((ARROW_OFFSET_X + ARROW_W) * scale, ARROW_OFFSET_Y * scale)
|
||||
.vec2(1, 0).color(c.r, c.g, c.b, c.a).next();
|
||||
ARROW_MESH.quad(i1, i2, i3, i4);
|
||||
ARROW_MESH.end();
|
||||
|
||||
Matrix4f transform = new Matrix4f()
|
||||
.translation((float) x, (float) y, 0.0f)
|
||||
.rotateZ(angleRad);
|
||||
|
||||
MeshRenderer.begin()
|
||||
.attachments(mc.gameRenderer.mainRenderTarget())
|
||||
.pipeline(LoVisualRenderPipelines.UI_TEXTURED_ADDITIVE)
|
||||
.mesh(ARROW_MESH)
|
||||
.transform(transform)
|
||||
.sampler("u_Texture", view, sampler)
|
||||
.end();
|
||||
}
|
||||
|
||||
@Override
|
||||
public WorldPhase getWorldPhase() {
|
||||
return WorldPhase.END_MAIN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HudPhase getHudPhase() {
|
||||
return HudPhase.BEFORE_MISC_OVERLAYS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
|
||||
if (!isEnabled() || mc.level == null || mc.player == null) return;
|
||||
if (!isLinesMode()) return;
|
||||
|
||||
Vec3 camPos = getActiveCameraPos(tickDelta);
|
||||
Vec3 look = getActiveCameraLook(tickDelta);
|
||||
Vec3 start = camPos.add(look.scale(10.0));
|
||||
|
||||
for (Player p : mc.level.players()) {
|
||||
if (p == mc.player) continue;
|
||||
if (!traceFriendsValue.get() && CategoryService.isFriend(p)) continue;
|
||||
|
||||
int c = resolveColor(p);
|
||||
Vec3 targetWorld = obtainEntityLerpedPos(p, tickDelta);
|
||||
|
||||
renderer.line(
|
||||
start.x, start.y, start.z,
|
||||
targetWorld.x, targetWorld.y, targetWorld.z,
|
||||
(c >> 16) & 255,
|
||||
(c >> 8) & 255,
|
||||
c & 255,
|
||||
(c >> 24) & 255
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRenderHudEngineForeground(Renderer2D renderer, TextRenderer textRenderer, net.minecraft.client.gui.GuiGraphicsExtractor ctx, float tickDelta) {
|
||||
if (!isEnabled() || mc.level == null || mc.player == null) return;
|
||||
if (!isArrowsMode()) return;
|
||||
if (!mc.options.getCameraType().isFirstPerson()) return;
|
||||
|
||||
Vec2 move = mc.player.input != null ? mc.player.input.getMoveVector() : Vec2.ZERO;
|
||||
float moveX = move.x;
|
||||
float moveZ = move.y;
|
||||
|
||||
float scale = ViewportContext.getScaleFactor();
|
||||
float moveScale = moveRadiusBonusValue.get() ? 10.0f : 0.0f;
|
||||
animatedYaw = AnimationUtility.fast(animatedYaw, moveX * moveScale * scale, 5.0f);
|
||||
animatedPitch = AnimationUtility.fast(animatedPitch, moveZ * moveScale * scale, 5.0f);
|
||||
|
||||
yaw = AnimationUtility.fast(yaw, getActiveCameraYaw(), 10.0f);
|
||||
|
||||
float size = BASE_RADIUS;
|
||||
if (ClientScreen.current() instanceof InventoryScreen) {
|
||||
size += INVENTORY_RADIUS_BONUS;
|
||||
}
|
||||
if (moveRadiusBonusValue.get() && isMoving(move)) {
|
||||
size += MOVE_RADIUS_BONUS;
|
||||
}
|
||||
animationStep = AnimationUtility.fast(animationStep, size * scale, 6.0f);
|
||||
|
||||
double centerX = mc.getWindow().getWidth() * 0.5;
|
||||
double centerY = mc.getWindow().getHeight() * 0.5;
|
||||
|
||||
Vec3 camPos = getActiveCameraPos(tickDelta);
|
||||
Identifier textureId = resolveArrowTexture();
|
||||
for (Player player : mc.level.players()) {
|
||||
if (player == mc.player) continue;
|
||||
if (!traceFriendsValue.get() && CategoryService.isFriend(player)) continue;
|
||||
|
||||
Vec3 targetWorld = obtainEntityLerpedPos(player, tickDelta);
|
||||
double relX = targetWorld.x - camPos.x;
|
||||
double relZ = targetWorld.z - camPos.z;
|
||||
|
||||
double yawRad = Math.toRadians(yaw);
|
||||
double cos = Math.cos(yawRad);
|
||||
double sin = Math.sin(yawRad);
|
||||
double rotY = -(relZ * cos - relX * sin);
|
||||
double rotX = -(relX * cos + relZ * sin);
|
||||
float angleDeg = (float) Math.toDegrees(Math.atan2(rotY, rotX));
|
||||
|
||||
double dist = targetWorld.distanceTo(camPos);
|
||||
float radiusFactor = shouldUseAdaptiveRadius() ? radiusFactorForDistance(dist) : 1.0f;
|
||||
double radius = animationStep * radiusFactor;
|
||||
double x2 = radius * Mth.cos((float) Math.toRadians(angleDeg)) + centerX + animatedYaw;
|
||||
double y2 = radius * Mth.sin((float) Math.toRadians(angleDeg)) + centerY + animatedPitch;
|
||||
|
||||
int color = withAlpha(resolveColor(player), ARROW_ALPHA);
|
||||
float rotRad = (float) Math.toRadians(angleDeg + ARROW_ROTATION_OFFSET_DEG);
|
||||
renderArrow(textureId, x2, y2, rotRad, color, scale);
|
||||
}
|
||||
}
|
||||
|
||||
private Vec3 getActiveCameraPos(float tickDelta) {
|
||||
Freecam fc = Modules.get(Freecam.class);
|
||||
if (fc != null && fc.isEnabled()) {
|
||||
return new Vec3(
|
||||
Mth.lerp(tickDelta, fc.camPosPrev.x, fc.camPos.x),
|
||||
Mth.lerp(tickDelta, fc.camPosPrev.y, fc.camPos.y),
|
||||
Mth.lerp(tickDelta, fc.camPosPrev.z, fc.camPos.z)
|
||||
);
|
||||
}
|
||||
return mc.gameRenderer.mainCamera().position();
|
||||
}
|
||||
|
||||
private Vec3 getActiveCameraLook(float tickDelta) {
|
||||
Freecam fc = Modules.get(Freecam.class);
|
||||
if (fc != null && fc.isEnabled()) {
|
||||
float yaw = (float) Math.toRadians(fc.camYaw);
|
||||
float pitch = (float) Math.toRadians(fc.camPitch);
|
||||
|
||||
return new Vec3(
|
||||
-Math.sin(yaw) * Math.cos(pitch),
|
||||
-Math.sin(pitch),
|
||||
Math.cos(yaw) * Math.cos(pitch)
|
||||
).normalize();
|
||||
}
|
||||
return mc.player.getViewVector(tickDelta).normalize();
|
||||
}
|
||||
|
||||
private float getActiveCameraYaw() {
|
||||
Freecam fc = Modules.get(Freecam.class);
|
||||
if (fc != null && fc.isEnabled()) {
|
||||
return fc.camYaw;
|
||||
}
|
||||
return mc.gameRenderer.mainCamera().yRot();
|
||||
}
|
||||
|
||||
public void render3DTracersRaw(
|
||||
Renderer3D renderer,
|
||||
float tickDelta,
|
||||
List<Vec3> targets,
|
||||
int argb
|
||||
) {
|
||||
if (mc.player == null || mc.level == null || targets == null || targets.isEmpty()) return;
|
||||
|
||||
Vec3 camPos = getActiveCameraPos(tickDelta);
|
||||
Vec3 look = getActiveCameraLook(tickDelta);
|
||||
Vec3 start = camPos.add(look.scale(10.0));
|
||||
|
||||
int a = (argb >> 24) & 255;
|
||||
int r = (argb >> 16) & 255;
|
||||
int g = (argb >> 8) & 255;
|
||||
int b = argb & 255;
|
||||
|
||||
for (Vec3 targetWorld : targets) {
|
||||
renderer.line(
|
||||
start.x, start.y, start.z,
|
||||
targetWorld.x, targetWorld.y, targetWorld.z,
|
||||
r, g, b, a
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private Vec3 obtainEntityLerpedPos(Entity e, float f) {
|
||||
try {
|
||||
return e.getPosition(f);
|
||||
} catch (Throwable t) {
|
||||
if (e instanceof IEntity a) {
|
||||
return a.get$InstantRenderPos().lerp(e.position(), f);
|
||||
}
|
||||
return e.position();
|
||||
}
|
||||
}
|
||||
|
||||
private int resolveColor(Player p) {
|
||||
return CategoryService.getColor(p);
|
||||
}
|
||||
|
||||
private boolean shouldUseAdaptiveRadius() {
|
||||
if (mc.player == null) return true;
|
||||
return !(ClientScreen.current() instanceof InventoryScreen);
|
||||
}
|
||||
|
||||
private Identifier resolveArrowTexture() {
|
||||
String mode = arrowTexture.get();
|
||||
if ("Arrow2".equalsIgnoreCase(mode)) return ARROW_2;
|
||||
if ("Arrow3".equalsIgnoreCase(mode)) return ARROW_3;
|
||||
if ("Arrow4".equalsIgnoreCase(mode)) return ARROW_4;
|
||||
return ARROW_1;
|
||||
}
|
||||
|
||||
private boolean isLinesMode() {
|
||||
return "Lines".equalsIgnoreCase(tracersMode.get());
|
||||
}
|
||||
|
||||
private boolean isArrowsMode() {
|
||||
return "Arrows".equalsIgnoreCase(tracersMode.get());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
|||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
import dev.loki.lovisual.features.command.CommandManager;
|
||||
import dev.loki.lovisual.features.gui.chat.BetterChatRenderer;
|
||||
import dev.loki.lovisual.features.gui.chat.BetterChatTabHandler;
|
||||
import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat;
|
||||
import dev.loki.lovisual.mixins.accessors.ChatScreenSuggestorAccessor;
|
||||
import dev.loki.lovisual.Lifecycle;
|
||||
|
|
@ -92,6 +93,15 @@ public class ChatScreenMixin {
|
|||
}
|
||||
}
|
||||
|
||||
@Inject(method = "keyPressed(Lnet/minecraft/client/input/KeyEvent;)Z", at = @At("HEAD"), cancellable = true, require = 0)
|
||||
private void lovisual$betterChat$tabCompleteCommands(KeyEvent input, CallbackInfoReturnable<Boolean> cir) {
|
||||
if (!Lifecycle.canRunHud()) return;
|
||||
if (!BetterChat.isActive()) return;
|
||||
if (BetterChatTabHandler.handleKey(input)) {
|
||||
cir.setReturnValue(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Unique
|
||||
private boolean lovisual$betterChat$shouldHideVanillaChat() {
|
||||
if (!Lifecycle.canRunHud()) return false;
|
||||
|
|
|
|||
|
|
@ -8,28 +8,19 @@
|
|||
package dev.loki.lovisual.mixins;
|
||||
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.particles.ItemParticleOption;
|
||||
import net.minecraft.core.particles.ParticleOptions;
|
||||
import net.minecraft.core.particles.ParticleTypes;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import dev.loki.lovisual.features.module.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.NoRender;
|
||||
import dev.loki.lovisual.util.block.BlockObservationHub;
|
||||
|
||||
@Mixin(ClientLevel.class)
|
||||
public abstract class ClientLevelMixin {
|
||||
|
||||
@Inject(method = "setServerVerifiedBlockState", at = @At("TAIL"))
|
||||
private void lovisual$recordBlockUpdate(BlockPos pos, BlockState state, @Block.UpdateFlags int flags, CallbackInfo ci) {
|
||||
BlockObservationHub.observeWorldUpdate(pos, state);
|
||||
}
|
||||
|
||||
@Inject(
|
||||
method = "doAddParticle(Lnet/minecraft/core/particles/ParticleOptions;ZZDDDDDD)V",
|
||||
at = @At("HEAD"),
|
||||
|
|
|
|||
|
|
@ -20,9 +20,6 @@ public abstract class LivingEntityRenderStateMixin implements ViewObstructionFad
|
|||
@Unique
|
||||
private float lovisual$viewObstructionFadeAlpha = 1.0f;
|
||||
|
||||
@Unique
|
||||
private boolean lovisual$seeInvisibleFadeActive;
|
||||
|
||||
@Override
|
||||
public boolean lovisual$isViewObstructionFadeActive() {
|
||||
return lovisual$viewObstructionFadeActive;
|
||||
|
|
@ -42,14 +39,4 @@ public abstract class LivingEntityRenderStateMixin implements ViewObstructionFad
|
|||
public void lovisual$setViewObstructionFadeAlpha(float alpha) {
|
||||
this.lovisual$viewObstructionFadeAlpha = alpha;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean lovisual$isSeeInvisibleFadeActive() {
|
||||
return lovisual$seeInvisibleFadeActive;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void lovisual$setSeeInvisibleFadeActive(boolean active) {
|
||||
this.lovisual$seeInvisibleFadeActive = active;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
|||
import dev.loki.lovisual.features.module.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.NameTags;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.NoRender;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.SeeInvisibles;
|
||||
import dev.loki.lovisual.render.ViewObstructionFadeContext;
|
||||
import dev.loki.lovisual.render.ViewObstructionFadeState;
|
||||
import dev.loki.lovisual.render.iris.IrisRuntime;
|
||||
|
|
@ -75,22 +74,10 @@ public abstract class LivingEntityRendererMixin<T extends LivingEntity, S extend
|
|||
float obstructionFadeAlpha = obstructionFadeActive ? noRender.getEntityFadeAlpha(entity) : 1.0f;
|
||||
obstructionFadeActive = obstructionFadeActive && obstructionFadeAlpha < 0.99f;
|
||||
|
||||
SeeInvisibles seeInvisibles = Modules.get(SeeInvisibles.class);
|
||||
boolean seeInvisibleActive = seeInvisibles != null && seeInvisibles.shouldRenderInvisiblePlayer(entity);
|
||||
float seeInvisibleAlpha = seeInvisibleActive ? seeInvisibles.getInvisiblePlayerAlpha01() : 1.0f;
|
||||
boolean seeInvisibleFadeActive = seeInvisibleActive && seeInvisibleAlpha < 0.99f;
|
||||
if (seeInvisibleActive) {
|
||||
state.isInvisibleToPlayer = false;
|
||||
}
|
||||
|
||||
boolean active = obstructionFadeActive || seeInvisibleFadeActive;
|
||||
boolean active = obstructionFadeActive;
|
||||
float alpha = obstructionFadeAlpha;
|
||||
if (seeInvisibleFadeActive) {
|
||||
alpha = Math.min(alpha, seeInvisibleAlpha);
|
||||
}
|
||||
fadeState.lovisual$setViewObstructionFadeActive(active);
|
||||
fadeState.lovisual$setViewObstructionFadeAlpha(active ? alpha : 1.0f);
|
||||
fadeState.lovisual$setSeeInvisibleFadeActive(seeInvisibleFadeActive);
|
||||
}
|
||||
|
||||
@ModifyExpressionValue(
|
||||
|
|
@ -169,21 +156,6 @@ public abstract class LivingEntityRendererMixin<T extends LivingEntity, S extend
|
|||
return RenderTypes.entityTranslucent(getTextureLocation(state), true);
|
||||
}
|
||||
|
||||
@ModifyConstant(
|
||||
method = "submit",
|
||||
constant = @Constant(intValue = 654311423)
|
||||
)
|
||||
private int lovisual$seeInvisibles$overrideInvisiblePlayerAlpha(int vanillaTint, S state) {
|
||||
SeeInvisibles seeInvisibles = Modules.get(SeeInvisibles.class);
|
||||
if (seeInvisibles != null
|
||||
&& state instanceof ViewObstructionFadeState fadeState
|
||||
&& fadeState.lovisual$isSeeInvisibleFadeActive()
|
||||
&& seeInvisibles.shouldRenderInvisiblePlayer(state.entityType, state.isInvisible)) {
|
||||
return seeInvisibles.getInvisiblePlayerTintArgb();
|
||||
}
|
||||
return vanillaTint;
|
||||
}
|
||||
|
||||
@Inject(method = "submit", at = @At("HEAD"))
|
||||
private void lovisual$pushViewObstructionFadeContext(S state, com.mojang.blaze3d.vertex.PoseStack matrixStack, net.minecraft.client.renderer.SubmitNodeCollector orderedRenderCommandQueue, net.minecraft.client.renderer.state.level.CameraRenderState cameraRenderState, CallbackInfo ci) {
|
||||
ViewObstructionFadeContext.push(state instanceof ViewObstructionFadeState fadeState ? fadeState : null);
|
||||
|
|
|
|||
|
|
@ -1,64 +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.mixins.sodium;
|
||||
|
||||
import net.caffeinemc.mods.sodium.client.render.chunk.terrain.material.Material;
|
||||
import net.caffeinemc.mods.sodium.client.render.model.MutableQuadViewImpl;
|
||||
import net.minecraft.client.renderer.block.dispatch.BlockStateModel;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Pseudo;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import dev.loki.lovisual.util.block.BlockObservationHub;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
|
||||
@Pseudo
|
||||
@Mixin(targets = "net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline.BlockRenderer")
|
||||
public abstract class SodiumBlockVisibilityMixin {
|
||||
@Unique
|
||||
private static final ThreadLocal<ArrayDeque<RenderedBlock>> LOVISUAL_BLOCK_STACK =
|
||||
ThreadLocal.withInitial(ArrayDeque::new);
|
||||
|
||||
@Inject(method = "renderModel", at = @At("HEAD"), remap = false)
|
||||
private void lovisual$pushRenderedBlock(BlockStateModel model, BlockState state, BlockPos pos, BlockPos origin, CallbackInfo ci) {
|
||||
LOVISUAL_BLOCK_STACK.get().addLast(new RenderedBlock(pos, state));
|
||||
}
|
||||
|
||||
@Inject(method = "renderModel", at = @At("RETURN"), remap = false)
|
||||
private void lovisual$popRenderedBlock(BlockStateModel model, BlockState state, BlockPos pos, BlockPos origin, CallbackInfo ci) {
|
||||
ArrayDeque<RenderedBlock> stack = LOVISUAL_BLOCK_STACK.get();
|
||||
if (!stack.isEmpty()) {
|
||||
stack.removeLast();
|
||||
}
|
||||
if (stack.isEmpty()) {
|
||||
LOVISUAL_BLOCK_STACK.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "bufferQuad", at = @At("HEAD"), remap = false)
|
||||
private void lovisual$observeBufferedQuad(MutableQuadViewImpl quad,
|
||||
float[] brightness,
|
||||
Material material,
|
||||
CallbackInfo ci) {
|
||||
ArrayDeque<RenderedBlock> stack = LOVISUAL_BLOCK_STACK.get();
|
||||
if (stack.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
RenderedBlock rendered = stack.peekLast();
|
||||
BlockObservationHub.observeSodiumRenderedBlock(rendered.pos(), rendered.state());
|
||||
}
|
||||
|
||||
private record RenderedBlock(BlockPos pos, BlockState state) {
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +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.mixins.sodium;
|
||||
|
||||
import com.llamalad7.mixinextras.sugar.Local;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Pseudo;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.ModifyVariable;
|
||||
import dev.loki.lovisual.util.block.BlockObservationHub;
|
||||
|
||||
@Pseudo
|
||||
@Mixin(targets = "net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderMeshingTask")
|
||||
public abstract class SodiumChunkBuilderMeshingTaskMixin {
|
||||
|
||||
@ModifyVariable(
|
||||
method = "execute(Lnet/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildContext;Lnet/caffeinemc/mods/sodium/client/util/task/CancellationToken;)Lnet/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildOutput;",
|
||||
at = @At("STORE"),
|
||||
ordinal = 0,
|
||||
remap = false
|
||||
)
|
||||
private BlockState lovisual$captureSodiumSectionBlock(BlockState state,
|
||||
@Local(index = 22) int x,
|
||||
@Local(index = 20) int y,
|
||||
@Local(index = 21) int z) {
|
||||
BlockObservationHub.observeSodiumBlock(x, y, z, state);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
|
@ -15,8 +15,4 @@ public interface ViewObstructionFadeState {
|
|||
void lovisual$setViewObstructionFadeActive(boolean active);
|
||||
|
||||
void lovisual$setViewObstructionFadeAlpha(float alpha);
|
||||
|
||||
boolean lovisual$isSeeInvisibleFadeActive();
|
||||
|
||||
void lovisual$setSeeInvisibleFadeActive(boolean active);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,177 +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.util.block;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Hot-path Sodium bridge for BlockESP.
|
||||
*
|
||||
* <p>This class is intentionally primitive-only on the meshing path: no module lookup,
|
||||
* no setting reads, no BlockPos allocation unless a configured block actually matched.</p>
|
||||
*/
|
||||
public enum BlockEspSodiumCandidateCollector {
|
||||
;
|
||||
private static final int MAX_QUEUED_CANDIDATES = 32768;
|
||||
|
||||
private static final Snapshot DISABLED = new Snapshot(false, Set.of(), false, 0, 0, 0, 0, 0);
|
||||
private static final ConcurrentLinkedQueue<Long> CANDIDATE_KEYS = new ConcurrentLinkedQueue<>();
|
||||
private static final Map<Long, Candidate> PENDING_CANDIDATES = new ConcurrentHashMap<>();
|
||||
private static final AtomicInteger QUEUED_COUNT = new AtomicInteger();
|
||||
private static volatile Snapshot snapshot = DISABLED;
|
||||
|
||||
public static void publishSnapshot(
|
||||
boolean enabled,
|
||||
Set<Block> targetBlocks,
|
||||
boolean limitDistance,
|
||||
int maxDistance,
|
||||
int centerX,
|
||||
int centerY,
|
||||
int centerZ,
|
||||
int generation
|
||||
) {
|
||||
if (!enabled || targetBlocks == null || targetBlocks.isEmpty()) {
|
||||
snapshot = DISABLED;
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
|
||||
snapshot = new Snapshot(
|
||||
true,
|
||||
Set.copyOf(targetBlocks),
|
||||
limitDistance,
|
||||
Math.max(1, maxDistance),
|
||||
centerX,
|
||||
centerY,
|
||||
centerZ,
|
||||
generation
|
||||
);
|
||||
}
|
||||
|
||||
public static void disable() {
|
||||
snapshot = DISABLED;
|
||||
clear();
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
CANDIDATE_KEYS.clear();
|
||||
PENDING_CANDIDATES.clear();
|
||||
QUEUED_COUNT.set(0);
|
||||
}
|
||||
|
||||
public static void observeSodiumBlock(int x, int y, int z, BlockState state) {
|
||||
offer(x, y, z, state, CandidateSource.SODIUM_SECTION_SCAN);
|
||||
}
|
||||
|
||||
public static void observeSodiumRenderedBlock(int x, int y, int z, BlockState state) {
|
||||
offer(x, y, z, state, CandidateSource.SODIUM_BUFFERED_QUAD);
|
||||
}
|
||||
|
||||
private static void offer(int x, int y, int z, BlockState state, CandidateSource source) {
|
||||
Snapshot current = snapshot;
|
||||
if (!current.enabled || state == null || state.isAir()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!current.targets.contains(state.getBlock())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (current.limitDistance && !current.withinDistance(x, y, z)) {
|
||||
return;
|
||||
}
|
||||
|
||||
long packedPos = BlockPos.asLong(x, y, z);
|
||||
Candidate candidate = new Candidate(packedPos, state, source, current.generation);
|
||||
while (true) {
|
||||
Candidate pending = PENDING_CANDIDATES.putIfAbsent(packedPos, candidate);
|
||||
if (pending == null) {
|
||||
break;
|
||||
}
|
||||
if (source != CandidateSource.SODIUM_BUFFERED_QUAD
|
||||
|| pending.source == CandidateSource.SODIUM_BUFFERED_QUAD) {
|
||||
return;
|
||||
}
|
||||
if (PENDING_CANDIDATES.replace(packedPos, pending, candidate)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int queued = QUEUED_COUNT.incrementAndGet();
|
||||
if (queued > MAX_QUEUED_CANDIDATES) {
|
||||
QUEUED_COUNT.decrementAndGet();
|
||||
PENDING_CANDIDATES.remove(packedPos, candidate);
|
||||
return;
|
||||
}
|
||||
|
||||
CANDIDATE_KEYS.add(packedPos);
|
||||
}
|
||||
|
||||
public static int drain(int maxCandidates, CandidateConsumer consumer) {
|
||||
if (maxCandidates <= 0 || consumer == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int drained = 0;
|
||||
while (drained < maxCandidates) {
|
||||
Long packedPos = CANDIDATE_KEYS.poll();
|
||||
if (packedPos == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
Candidate candidate = PENDING_CANDIDATES.remove(packedPos);
|
||||
if (candidate == null) {
|
||||
continue;
|
||||
}
|
||||
QUEUED_COUNT.decrementAndGet();
|
||||
consumer.accept(candidate);
|
||||
drained++;
|
||||
}
|
||||
return drained;
|
||||
}
|
||||
|
||||
public enum CandidateSource {
|
||||
SODIUM_SECTION_SCAN,
|
||||
SODIUM_BUFFERED_QUAD
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface CandidateConsumer {
|
||||
void accept(Candidate candidate);
|
||||
}
|
||||
|
||||
private record Snapshot(
|
||||
boolean enabled,
|
||||
Set<Block> targets,
|
||||
boolean limitDistance,
|
||||
int maxDistance,
|
||||
int centerX,
|
||||
int centerY,
|
||||
int centerZ,
|
||||
int generation
|
||||
) {
|
||||
boolean withinDistance(int x, int y, int z) {
|
||||
long dx = (long) x - centerX;
|
||||
long dy = (long) y - centerY;
|
||||
long dz = (long) z - centerZ;
|
||||
long max = maxDistance;
|
||||
return dx * dx + dy * dy + dz * dz <= max * max;
|
||||
}
|
||||
}
|
||||
|
||||
public record Candidate(long packedPos, BlockState state, CandidateSource source, int generation) {
|
||||
}
|
||||
}
|
||||
|
|
@ -1,103 +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.util.block;
|
||||
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import dev.loki.lovisual.features.module.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.BedwarsESP;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.BlockESP;
|
||||
import dev.loki.lovisual.util.block.bed.BedBlockUtil;
|
||||
|
||||
public enum BlockObservationHub {
|
||||
;
|
||||
|
||||
public static void requestSodiumRebuild(Minecraft client, int requestedRadius) {
|
||||
if (client == null || client.level == null || client.player == null) {
|
||||
return;
|
||||
}
|
||||
if (!FabricLoader.getInstance().isModLoaded("sodium")) {
|
||||
return;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (state == null || state.isAir()) {
|
||||
return;
|
||||
}
|
||||
|
||||
BlockEspSodiumCandidateCollector.observeSodiumBlock(x, y, z, state);
|
||||
|
||||
if (BedBlockUtil.isBed(state)) {
|
||||
BedwarsESP bedwarsEsp = Modules.get(BedwarsESP.class);
|
||||
if (bedwarsEsp != null && bedwarsEsp.isEnabled()) {
|
||||
bedwarsEsp.acceptSodiumBlockState(x, y, z, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void observeSodiumRenderedBlock(BlockPos pos, BlockState state) {
|
||||
if (pos == null || state == null || state.isAir()) {
|
||||
return;
|
||||
}
|
||||
|
||||
BlockEspSodiumCandidateCollector.observeSodiumRenderedBlock(pos.getX(), pos.getY(), pos.getZ(), state);
|
||||
|
||||
if (BedBlockUtil.isBed(state)) {
|
||||
BedwarsESP bedwarsEsp = Modules.get(BedwarsESP.class);
|
||||
if (bedwarsEsp != null && bedwarsEsp.isEnabled()) {
|
||||
bedwarsEsp.acceptSodiumRenderedBlockState(pos, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void observeWorldUpdate(BlockPos pos, BlockState state) {
|
||||
if (pos == null || state == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
BlockESP blockEsp = Modules.get(BlockESP.class);
|
||||
if (blockEsp != null && blockEsp.isEnabled()) {
|
||||
blockEsp.acceptWorldBlockUpdate(pos, state);
|
||||
}
|
||||
|
||||
BedwarsESP bedwarsEsp = Modules.get(BedwarsESP.class);
|
||||
if (bedwarsEsp != null && bedwarsEsp.isEnabled()) {
|
||||
bedwarsEsp.acceptWorldBlockUpdate(pos, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -545,7 +545,6 @@
|
|||
"setting.freecam.vertical_speed": "Vertical speed",
|
||||
"setting.freecam.toggle_input": "Toggle input",
|
||||
"setting.fullbright.min_light": "Min light",
|
||||
"setting.seeinvisibles.alpha": "Alpha",
|
||||
"setting.soundesp.name_mode": "Name mode",
|
||||
"setting.soundesp.name_mode.option.Technical": "Technical",
|
||||
"setting.soundesp.name_mode.option.Localized": "Localized",
|
||||
|
|
@ -833,20 +832,6 @@
|
|||
"setting.crosshair.orbiz_move_multiplier": "Move multiplier",
|
||||
"setting.crosshair.orbiz_motion_limit": "Motion limit",
|
||||
"setting.crosshair.orbiz_motion_smoothing": "Motion smoothing",
|
||||
"setting.tracers.trace_friends": "Trace friends",
|
||||
"setting.tracers.move_radius_bonus": "Movement radius bonus",
|
||||
"setting.tracers.mode": "Mode",
|
||||
"setting.tracers.mode.option.Lines": "Lines",
|
||||
"setting.tracers.mode.option.Arrows": "Arrows",
|
||||
"setting.tracers.arrow_texture": "Arrow texture",
|
||||
"setting.tracers.arrow_texture.option.Arrow": "Arrow 1",
|
||||
"setting.tracers.arrow_texture.option.Arrow2": "Arrow 2",
|
||||
"setting.tracers.arrow_texture.option.Arrow3": "Arrow 3",
|
||||
"setting.tracers.arrow_texture.option.Arrow4": "Arrow 4",
|
||||
"setting.tracers.default_color": "Default color",
|
||||
"setting.tracers.friend_color": "Friend color",
|
||||
"setting.tracers.enemy_color": "Enemy color",
|
||||
"setting.tracers.staff_color": "Staff color",
|
||||
"setting.trails.only_self": "Only self",
|
||||
"setting.trails.hide_first_person": "Hide in first person",
|
||||
"setting.trails.depth_test": "Depth test",
|
||||
|
|
@ -1309,21 +1294,8 @@
|
|||
"setting.blockhighlight.breaking_animation.option.static": "Static",
|
||||
"setting.blockhighlight.breaking_color": "Breaking color",
|
||||
"setting.blockhighlight.breaking_color2": "Second breaking color",
|
||||
"setting.bedwarsesp.self_bed": "Self bed",
|
||||
"setting.bedwarsesp.self_bed.option.none": "None",
|
||||
"setting.bedwarsesp.self_bed.option.color": "By color",
|
||||
"setting.bedwarsesp.self_bed.option.spawn_location": "By spawn",
|
||||
"setting.bedwarsesp.spawn_bed_distance": "Spawn bed distance",
|
||||
"setting.bedwarsesp.max_layers": "Max layers",
|
||||
"setting.bedwarsesp.show_openings": "Show openings",
|
||||
"setting.bedwarsesp.self_bed_color": "Self bed color",
|
||||
"setting.bedwarsesp.other_bed_color": "Other bed color",
|
||||
"setting.bedwarsesp.self_opening_color": "Self opening color",
|
||||
"setting.bedwarsesp.other_opening_color": "Other opening color",
|
||||
"setting.blockesp.search_blocks": "Search blocks",
|
||||
"setting.blockesp.visible_color": "Visible color",
|
||||
"setting.blockesp.chunk_color": "Chunk color",
|
||||
"setting.blockesp.cluster_tracers": "Cluster tracers",
|
||||
"setting.invisesp.color": "Color",
|
||||
"setting.invisesp.line_width": "Line width",
|
||||
"setting.holeesp.range_xz": "Range XZ",
|
||||
"setting.holeesp.range_y": "Range Y",
|
||||
"setting.holeesp.height": "Height",
|
||||
|
|
@ -1938,6 +1910,7 @@
|
|||
"setting.draggable.triangulator.triangulator_cache_lifetime": "Cache lifetime",
|
||||
"setting.draggable.triangulator.triangulator_auto_reset": "Auto reset",
|
||||
"module.autobed.description": "Automatically places and detonates beds in explosive dimensions.",
|
||||
"module.invisesp.description": "Outlines players hidden by invisibility.",
|
||||
"module.holeesp.description": "Highlights safe holes with a distance-faded fill.",
|
||||
"setting.autobed.place": "Place",
|
||||
"setting.autobed.explode": "Explode",
|
||||
|
|
@ -3221,5 +3194,19 @@
|
|||
"command.toggle.description": "Enables, disables, or toggles a module.",
|
||||
"command.tps.description": "Shows estimated server ticks per second.",
|
||||
"command.username.description": "Shows the current Minecraft username and UUID.",
|
||||
"command.xaero.description": "Adds, lists, or removes Xaero waypoints."
|
||||
"command.xaero.description": "Adds, lists, or removes Xaero waypoints.",
|
||||
"module.chinahat.description": "Renders a classic conical Chinese hat on players' heads.",
|
||||
"setting.chinahat.render_target": "Render",
|
||||
"setting.chinahat.render_target.option.self": "Self",
|
||||
"setting.chinahat.render_target.option.players": "Players",
|
||||
"setting.chinahat.render_target.option.both": "Both",
|
||||
"setting.chinahat.rim_color": "Rim color",
|
||||
"setting.chinahat.segments": "Segments",
|
||||
"setting.chinahat.brim_radius": "Brim radius",
|
||||
"setting.chinahat.dome_radius": "Dome radius",
|
||||
"setting.chinahat.dome_height": "Dome height",
|
||||
"setting.chinahat.brim_thickness": "Brim thickness",
|
||||
"setting.chinahat.head_top_offset": "Head height",
|
||||
"setting.chinahat.head_forward_offset": "Forward offset",
|
||||
"setting.chinahat.head_right_offset": "Right offset"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -480,7 +480,6 @@
|
|||
"setting.freecam.vertical_speed": "Вертикальная скорость",
|
||||
"setting.freecam.toggle_input": "Переключить ввод",
|
||||
"setting.fullbright.min_light": "Мин. свет",
|
||||
"setting.seeinvisibles.alpha": "Альфа",
|
||||
"setting.soundesp.name_mode": "Режим имени",
|
||||
"setting.soundesp.name_mode.option.Technical": "Технический",
|
||||
"setting.soundesp.name_mode.option.Localized": "Локализованный",
|
||||
|
|
@ -768,20 +767,6 @@
|
|||
"setting.crosshair.orbiz_move_multiplier": "Множитель движения",
|
||||
"setting.crosshair.orbiz_motion_limit": "Лимит движения",
|
||||
"setting.crosshair.orbiz_motion_smoothing": "Сглаживание движения",
|
||||
"setting.tracers.trace_friends": "Трейсить друзей",
|
||||
"setting.tracers.move_radius_bonus": "Бонус радиуса от движения",
|
||||
"setting.tracers.mode": "Режим",
|
||||
"setting.tracers.mode.option.Lines": "Линии",
|
||||
"setting.tracers.mode.option.Arrows": "Стрелки",
|
||||
"setting.tracers.arrow_texture": "Текстура стрелки",
|
||||
"setting.tracers.arrow_texture.option.Arrow": "Стрела 1",
|
||||
"setting.tracers.arrow_texture.option.Arrow2": "Стрела 2",
|
||||
"setting.tracers.arrow_texture.option.Arrow3": "Стрела 3",
|
||||
"setting.tracers.arrow_texture.option.Arrow4": "Стрела 4",
|
||||
"setting.tracers.default_color": "Цвет обычных",
|
||||
"setting.tracers.friend_color": "Цвет друзей",
|
||||
"setting.tracers.enemy_color": "Цвет врагов",
|
||||
"setting.tracers.staff_color": "Цвет стаффа",
|
||||
"setting.trails.only_self": "Только себя",
|
||||
"setting.trails.hide_first_person": "Скрывать в первом лице",
|
||||
"setting.trails.depth_test": "Глубинный тест",
|
||||
|
|
@ -1103,22 +1088,9 @@
|
|||
"setting.blockhighlight.breaking_animation.option.static": "Статичная",
|
||||
"setting.blockhighlight.breaking_color": "Цвет ломания",
|
||||
"setting.blockhighlight.breaking_color2": "Второй цвет ломания",
|
||||
"setting.bedwarsesp.self_bed": "Своя кровать",
|
||||
"setting.bedwarsesp.self_bed.option.none": "Нет",
|
||||
"setting.bedwarsesp.self_bed.option.color": "По цвету",
|
||||
"setting.bedwarsesp.self_bed.option.spawn_location": "По спавну",
|
||||
"setting.bedwarsesp.spawn_bed_distance": "Дистанция кровати от спавна",
|
||||
"setting.bedwarsesp.max_layers": "Макс. слоёв",
|
||||
"setting.bedwarsesp.show_openings": "Показывать вскрытия",
|
||||
"setting.bedwarsesp.self_bed_color": "Цвет своей кровати",
|
||||
"setting.bedwarsesp.other_bed_color": "Цвет чужой кровати",
|
||||
"setting.bedwarsesp.self_opening_color": "Цвет вскрытия своей кровати",
|
||||
"setting.bedwarsesp.other_opening_color": "Цвет вскрытия чужой кровати",
|
||||
"setting.aspectratio.aspect_multiplier": "Множитель соотношения сторон",
|
||||
"setting.blockesp.search_blocks": "Искомые блоки",
|
||||
"setting.blockesp.visible_color": "Цвет видимых",
|
||||
"setting.blockesp.chunk_color": "Цвет чанка",
|
||||
"setting.blockesp.cluster_tracers": "Кластерные трейсеры",
|
||||
"setting.invisesp.color": "Цвет",
|
||||
"setting.invisesp.line_width": "Толщина линии",
|
||||
"setting.holeesp.range_xz": "Радиус XZ",
|
||||
"setting.holeesp.range_y": "Радиус Y",
|
||||
"setting.holeesp.height": "Высота",
|
||||
|
|
@ -1733,6 +1705,7 @@
|
|||
"setting.draggable.triangulator.triangulator_cache_lifetime": "Время жизни кеша",
|
||||
"setting.draggable.triangulator.triangulator_auto_reset": "Автосброс",
|
||||
"module.autobed.description": "Автоматически ставит и взрывает кровати в измерениях, где они взрываются.",
|
||||
"module.invisesp.description": "Обводит игроков, скрытых невидимостью.",
|
||||
"module.holeesp.description": "Подсвечивает безопасные холки с фейдом по дистанции.",
|
||||
"setting.autobed.place": "Ставить",
|
||||
"setting.autobed.explode": "Взрывать",
|
||||
|
|
@ -3011,5 +2984,19 @@
|
|||
"command.toggle.description": "Включает, отключает или переключает модуль.",
|
||||
"command.tps.description": "Показывает расчётное количество серверных тиков в секунду.",
|
||||
"command.username.description": "Показывает текущее имя Minecraft и UUID.",
|
||||
"command.xaero.description": "Добавляет, показывает или удаляет точки Xaero."
|
||||
"command.xaero.description": "Добавляет, показывает или удаляет точки Xaero.",
|
||||
"module.chinahat.description": "Рисует классическую коническую китайскую шляпу на головах игроков.",
|
||||
"setting.chinahat.render_target": "Рендер",
|
||||
"setting.chinahat.render_target.option.self": "Себе",
|
||||
"setting.chinahat.render_target.option.players": "Игрокам",
|
||||
"setting.chinahat.render_target.option.both": "Всем",
|
||||
"setting.chinahat.rim_color": "Цвет обода",
|
||||
"setting.chinahat.segments": "Сегменты",
|
||||
"setting.chinahat.brim_radius": "Радиус полей",
|
||||
"setting.chinahat.dome_radius": "Радиус купола",
|
||||
"setting.chinahat.dome_height": "Высота купола",
|
||||
"setting.chinahat.brim_thickness": "Толщина полей",
|
||||
"setting.chinahat.head_top_offset": "Высота на голове",
|
||||
"setting.chinahat.head_forward_offset": "Смещение вперёд",
|
||||
"setting.chinahat.head_right_offset": "Смещение вправо"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -171,9 +171,7 @@
|
|||
"security.TranslatableTextContentMixin",
|
||||
"security.TranslationStorageMixin",
|
||||
"sodium.SodiumBlockRendererMixin",
|
||||
"sodium.SodiumBlockVisibilityMixin",
|
||||
"sodium.SodiumChunkMeshFormatsMixin",
|
||||
"sodium.SodiumChunkBuilderMeshingTaskMixin",
|
||||
"sodium.SodiumChunkVertexMixin",
|
||||
"sodium.SodiumShaderChunkRendererMixin",
|
||||
"sodium.SodiumVertexConsumerTrackerMixin",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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.module.modules.visuals;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
final class ChinaHatPaletteTest {
|
||||
@Test
|
||||
void channelsAreIsolatedAndPreserved() {
|
||||
int argb = 0xA1B2C3D4;
|
||||
|
||||
assertEquals(0xA1, ChinaHatPalette.alpha(argb));
|
||||
assertEquals(0xB2, ChinaHatPalette.red(argb));
|
||||
assertEquals(0xC3, ChinaHatPalette.green(argb));
|
||||
assertEquals(0xD4, ChinaHatPalette.blue(argb));
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiplyRgbKeepsAlphaAndClamps() {
|
||||
int argb = 0xFF804020;
|
||||
|
||||
assertEquals(0xFFC06030, ChinaHatPalette.multiplyRgb(argb, 1.5f));
|
||||
assertEquals(0xFF000000, ChinaHatPalette.multiplyRgb(argb, 0.0f));
|
||||
assertEquals(0xFFFFFF80, ChinaHatPalette.multiplyRgb(argb, 4.0f));
|
||||
}
|
||||
|
||||
@Test
|
||||
void brightenUsesSameScalingAsMultiply() {
|
||||
int argb = 0x80FF8000;
|
||||
|
||||
assertEquals(
|
||||
ChinaHatPalette.multiplyRgb(argb, 1.25f),
|
||||
ChinaHatPalette.brighten(argb, 1.25f)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withAlphaReplacesOnlyTheAlphaChannel() {
|
||||
int argb = 0xFF123456;
|
||||
|
||||
assertEquals(0x50123456, ChinaHatPalette.withAlpha(argb, 80));
|
||||
assertEquals(0xFF123456, ChinaHatPalette.withAlpha(argb, 999));
|
||||
assertEquals(0x00123456, ChinaHatPalette.withAlpha(argb, -5));
|
||||
}
|
||||
|
||||
@Test
|
||||
void alphaOrFallsBackOnlyWhenChannelIsZero() {
|
||||
assertEquals(238, ChinaHatPalette.alphaOr(0x00123456, 238));
|
||||
assertEquals(255, ChinaHatPalette.alphaOr(0xFF123456, 238));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mixClampsAndBlendsEveryChannel() {
|
||||
int a = 0xFF000000;
|
||||
int b = 0x00FFFFFF;
|
||||
|
||||
assertEquals(a, ChinaHatPalette.mix(a, b, -1.0f));
|
||||
assertEquals(b, ChinaHatPalette.mix(a, b, 2.0f));
|
||||
assertEquals(0x80808080, ChinaHatPalette.mix(a, b, 0.5f));
|
||||
}
|
||||
|
||||
@Test
|
||||
void colorModeIdsAreUniqueAndLowercase() {
|
||||
String[] ids = java.util.Arrays.stream(ChinaHat.ColorMode.values())
|
||||
.map(ChinaHat.ColorMode::id)
|
||||
.toArray(String[]::new);
|
||||
|
||||
assertEquals(ChinaHat.ColorMode.values().length, java.util.Arrays.stream(ids).distinct().count());
|
||||
for (String id : ids) {
|
||||
assertEquals(id, id.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void renderTargetIdsAreUnique() {
|
||||
String[] ids = java.util.Arrays.stream(ChinaHat.RenderTarget.values())
|
||||
.map(ChinaHat.RenderTarget::id)
|
||||
.toArray(String[]::new);
|
||||
|
||||
assertEquals(ChinaHat.RenderTarget.values().length, java.util.Arrays.stream(ids).distinct().count());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue