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.svg.SvgRenderOptions;
|
||||||
import dev.loki.lovisual.render.engine.text.TextGlyphFallback;
|
import dev.loki.lovisual.render.engine.text.TextGlyphFallback;
|
||||||
import dev.loki.lovisual.render.engine.text.TextRenderer;
|
import dev.loki.lovisual.render.engine.text.TextRenderer;
|
||||||
|
import dev.loki.lovisual.util.time.GameClock;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
|
|
||||||
import java.util.List;
|
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 - 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);
|
||||||
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) -> {
|
tr.renderQuadGradient(text.toString(), runX, textY, (idx, cp, x0, y0, x1, y1, out) -> {
|
||||||
float t0 = (float) ((x0 - runX) / runW);
|
float t0 = (float) ((x0 - runX) / runW) + flow;
|
||||||
float t1 = (float) ((x1 - runX) / runW);
|
float t1 = (float) ((x1 - runX) / runW) + flow;
|
||||||
int leftTop = BetterChatDrawUtil.mulAlpha(CommandOutput.mixRgb(start, end, t0), alpha);
|
if (t0 > 1.0f) t0 -= 1.0f;
|
||||||
int rightTop = BetterChatDrawUtil.mulAlpha(CommandOutput.mixRgb(start, end, t1), alpha);
|
if (t1 > 1.0f) t1 -= 1.0f;
|
||||||
int leftBottom = BetterChatDrawUtil.mulAlpha(CommandOutput.mixRgb(start, bottomMix, t0), alpha);
|
int base0 = CommandOutput.mixRgb(start, end, t0);
|
||||||
int rightBottom = BetterChatDrawUtil.mulAlpha(CommandOutput.mixRgb(start, bottomMix, t1), alpha);
|
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[0] = leftTop;
|
||||||
out[1] = leftBottom;
|
out[1] = leftBottom;
|
||||||
out[2] = rightBottom;
|
out[2] = rightBottom;
|
||||||
|
|
@ -145,4 +154,8 @@ final class BetterChatGlyphRenderer {
|
||||||
tr.end();
|
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.
|
* 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;
|
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;
|
import dev.loki.lovisual.render.engine.uniform.impl.PostProcessUniforms;
|
||||||
|
|
||||||
//todo Description
|
//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 {
|
public class DamageTint extends Module implements PostProcessPass {
|
||||||
|
|
||||||
private static final String SETTING_START_HP = "start_hp";
|
private static final String SETTING_START_HP = "start_hp";
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
* Licensed under the GNU General Public License v3.0.
|
* 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.pipeline.RenderPipeline;
|
||||||
import com.mojang.blaze3d.vertex.PoseStack;
|
import com.mojang.blaze3d.vertex.PoseStack;
|
||||||
|
|
@ -69,7 +69,7 @@ import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
//todo Description
|
//todo Description
|
||||||
@ModuleInfo(id = "predictions", displayName = "Predictions", category = ModuleCategory.VISUALS)
|
@ModuleInfo(id = "predictions", displayName = "Predictions", category = ModuleCategory.COMBAT)
|
||||||
public class Predictions extends Module {
|
public class Predictions extends Module {
|
||||||
private static final String SETTING_LINE_COLOR = "line_color";
|
private static final String SETTING_LINE_COLOR = "line_color";
|
||||||
private static final String SETTING_PEARL_CUSTOM_COLOR = "pearl_custom_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.
|
* 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;
|
import dev.loki.lovisual.util.time.GameClock;
|
||||||
|
|
||||||
|
|
@ -45,7 +45,7 @@ import java.util.List;
|
||||||
import java.util.function.IntFunction;
|
import java.util.function.IntFunction;
|
||||||
|
|
||||||
//todo Description
|
//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 {
|
public class TargetESP extends Module {
|
||||||
|
|
||||||
private static final String SETTING_MODE = "mode";
|
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 org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||||
import dev.loki.lovisual.features.command.CommandManager;
|
import dev.loki.lovisual.features.command.CommandManager;
|
||||||
import dev.loki.lovisual.features.gui.chat.BetterChatRenderer;
|
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.features.gui.hud.draggable.impl.BetterChat;
|
||||||
import dev.loki.lovisual.mixins.accessors.ChatScreenSuggestorAccessor;
|
import dev.loki.lovisual.mixins.accessors.ChatScreenSuggestorAccessor;
|
||||||
import dev.loki.lovisual.Lifecycle;
|
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
|
@Unique
|
||||||
private boolean lovisual$betterChat$shouldHideVanillaChat() {
|
private boolean lovisual$betterChat$shouldHideVanillaChat() {
|
||||||
if (!Lifecycle.canRunHud()) return false;
|
if (!Lifecycle.canRunHud()) return false;
|
||||||
|
|
|
||||||
|
|
@ -8,28 +8,19 @@
|
||||||
package dev.loki.lovisual.mixins;
|
package dev.loki.lovisual.mixins;
|
||||||
|
|
||||||
import net.minecraft.client.multiplayer.ClientLevel;
|
import net.minecraft.client.multiplayer.ClientLevel;
|
||||||
import net.minecraft.core.BlockPos;
|
|
||||||
import net.minecraft.core.particles.ItemParticleOption;
|
import net.minecraft.core.particles.ItemParticleOption;
|
||||||
import net.minecraft.core.particles.ParticleOptions;
|
import net.minecraft.core.particles.ParticleOptions;
|
||||||
import net.minecraft.core.particles.ParticleTypes;
|
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.Mixin;
|
||||||
import org.spongepowered.asm.mixin.injection.At;
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
import org.spongepowered.asm.mixin.injection.Inject;
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||||
import dev.loki.lovisual.features.module.Modules;
|
import dev.loki.lovisual.features.module.Modules;
|
||||||
import dev.loki.lovisual.features.module.modules.visuals.NoRender;
|
import dev.loki.lovisual.features.module.modules.visuals.NoRender;
|
||||||
import dev.loki.lovisual.util.block.BlockObservationHub;
|
|
||||||
|
|
||||||
@Mixin(ClientLevel.class)
|
@Mixin(ClientLevel.class)
|
||||||
public abstract class ClientLevelMixin {
|
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(
|
@Inject(
|
||||||
method = "doAddParticle(Lnet/minecraft/core/particles/ParticleOptions;ZZDDDDDD)V",
|
method = "doAddParticle(Lnet/minecraft/core/particles/ParticleOptions;ZZDDDDDD)V",
|
||||||
at = @At("HEAD"),
|
at = @At("HEAD"),
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,6 @@ public abstract class LivingEntityRenderStateMixin implements ViewObstructionFad
|
||||||
@Unique
|
@Unique
|
||||||
private float lovisual$viewObstructionFadeAlpha = 1.0f;
|
private float lovisual$viewObstructionFadeAlpha = 1.0f;
|
||||||
|
|
||||||
@Unique
|
|
||||||
private boolean lovisual$seeInvisibleFadeActive;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean lovisual$isViewObstructionFadeActive() {
|
public boolean lovisual$isViewObstructionFadeActive() {
|
||||||
return lovisual$viewObstructionFadeActive;
|
return lovisual$viewObstructionFadeActive;
|
||||||
|
|
@ -42,14 +39,4 @@ public abstract class LivingEntityRenderStateMixin implements ViewObstructionFad
|
||||||
public void lovisual$setViewObstructionFadeAlpha(float alpha) {
|
public void lovisual$setViewObstructionFadeAlpha(float alpha) {
|
||||||
this.lovisual$viewObstructionFadeAlpha = 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;
|
||||||
import dev.loki.lovisual.features.module.modules.visuals.NameTags;
|
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.NoRender;
|
||||||
import dev.loki.lovisual.features.module.modules.visuals.SeeInvisibles;
|
|
||||||
import dev.loki.lovisual.render.ViewObstructionFadeContext;
|
import dev.loki.lovisual.render.ViewObstructionFadeContext;
|
||||||
import dev.loki.lovisual.render.ViewObstructionFadeState;
|
import dev.loki.lovisual.render.ViewObstructionFadeState;
|
||||||
import dev.loki.lovisual.render.iris.IrisRuntime;
|
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;
|
float obstructionFadeAlpha = obstructionFadeActive ? noRender.getEntityFadeAlpha(entity) : 1.0f;
|
||||||
obstructionFadeActive = obstructionFadeActive && obstructionFadeAlpha < 0.99f;
|
obstructionFadeActive = obstructionFadeActive && obstructionFadeAlpha < 0.99f;
|
||||||
|
|
||||||
SeeInvisibles seeInvisibles = Modules.get(SeeInvisibles.class);
|
boolean active = obstructionFadeActive;
|
||||||
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;
|
|
||||||
float alpha = obstructionFadeAlpha;
|
float alpha = obstructionFadeAlpha;
|
||||||
if (seeInvisibleFadeActive) {
|
|
||||||
alpha = Math.min(alpha, seeInvisibleAlpha);
|
|
||||||
}
|
|
||||||
fadeState.lovisual$setViewObstructionFadeActive(active);
|
fadeState.lovisual$setViewObstructionFadeActive(active);
|
||||||
fadeState.lovisual$setViewObstructionFadeAlpha(active ? alpha : 1.0f);
|
fadeState.lovisual$setViewObstructionFadeAlpha(active ? alpha : 1.0f);
|
||||||
fadeState.lovisual$setSeeInvisibleFadeActive(seeInvisibleFadeActive);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ModifyExpressionValue(
|
@ModifyExpressionValue(
|
||||||
|
|
@ -169,21 +156,6 @@ public abstract class LivingEntityRendererMixin<T extends LivingEntity, S extend
|
||||||
return RenderTypes.entityTranslucent(getTextureLocation(state), true);
|
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"))
|
@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) {
|
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);
|
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$setViewObstructionFadeActive(boolean active);
|
||||||
|
|
||||||
void lovisual$setViewObstructionFadeAlpha(float alpha);
|
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.vertical_speed": "Vertical speed",
|
||||||
"setting.freecam.toggle_input": "Toggle input",
|
"setting.freecam.toggle_input": "Toggle input",
|
||||||
"setting.fullbright.min_light": "Min light",
|
"setting.fullbright.min_light": "Min light",
|
||||||
"setting.seeinvisibles.alpha": "Alpha",
|
|
||||||
"setting.soundesp.name_mode": "Name mode",
|
"setting.soundesp.name_mode": "Name mode",
|
||||||
"setting.soundesp.name_mode.option.Technical": "Technical",
|
"setting.soundesp.name_mode.option.Technical": "Technical",
|
||||||
"setting.soundesp.name_mode.option.Localized": "Localized",
|
"setting.soundesp.name_mode.option.Localized": "Localized",
|
||||||
|
|
@ -833,20 +832,6 @@
|
||||||
"setting.crosshair.orbiz_move_multiplier": "Move multiplier",
|
"setting.crosshair.orbiz_move_multiplier": "Move multiplier",
|
||||||
"setting.crosshair.orbiz_motion_limit": "Motion limit",
|
"setting.crosshair.orbiz_motion_limit": "Motion limit",
|
||||||
"setting.crosshair.orbiz_motion_smoothing": "Motion smoothing",
|
"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.only_self": "Only self",
|
||||||
"setting.trails.hide_first_person": "Hide in first person",
|
"setting.trails.hide_first_person": "Hide in first person",
|
||||||
"setting.trails.depth_test": "Depth test",
|
"setting.trails.depth_test": "Depth test",
|
||||||
|
|
@ -1309,21 +1294,8 @@
|
||||||
"setting.blockhighlight.breaking_animation.option.static": "Static",
|
"setting.blockhighlight.breaking_animation.option.static": "Static",
|
||||||
"setting.blockhighlight.breaking_color": "Breaking color",
|
"setting.blockhighlight.breaking_color": "Breaking color",
|
||||||
"setting.blockhighlight.breaking_color2": "Second breaking color",
|
"setting.blockhighlight.breaking_color2": "Second breaking color",
|
||||||
"setting.bedwarsesp.self_bed": "Self bed",
|
"setting.invisesp.color": "Color",
|
||||||
"setting.bedwarsesp.self_bed.option.none": "None",
|
"setting.invisesp.line_width": "Line width",
|
||||||
"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.holeesp.range_xz": "Range XZ",
|
"setting.holeesp.range_xz": "Range XZ",
|
||||||
"setting.holeesp.range_y": "Range Y",
|
"setting.holeesp.range_y": "Range Y",
|
||||||
"setting.holeesp.height": "Height",
|
"setting.holeesp.height": "Height",
|
||||||
|
|
@ -1938,6 +1910,7 @@
|
||||||
"setting.draggable.triangulator.triangulator_cache_lifetime": "Cache lifetime",
|
"setting.draggable.triangulator.triangulator_cache_lifetime": "Cache lifetime",
|
||||||
"setting.draggable.triangulator.triangulator_auto_reset": "Auto reset",
|
"setting.draggable.triangulator.triangulator_auto_reset": "Auto reset",
|
||||||
"module.autobed.description": "Automatically places and detonates beds in explosive dimensions.",
|
"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.",
|
"module.holeesp.description": "Highlights safe holes with a distance-faded fill.",
|
||||||
"setting.autobed.place": "Place",
|
"setting.autobed.place": "Place",
|
||||||
"setting.autobed.explode": "Explode",
|
"setting.autobed.explode": "Explode",
|
||||||
|
|
@ -3221,5 +3194,19 @@
|
||||||
"command.toggle.description": "Enables, disables, or toggles a module.",
|
"command.toggle.description": "Enables, disables, or toggles a module.",
|
||||||
"command.tps.description": "Shows estimated server ticks per second.",
|
"command.tps.description": "Shows estimated server ticks per second.",
|
||||||
"command.username.description": "Shows the current Minecraft username and UUID.",
|
"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.vertical_speed": "Вертикальная скорость",
|
||||||
"setting.freecam.toggle_input": "Переключить ввод",
|
"setting.freecam.toggle_input": "Переключить ввод",
|
||||||
"setting.fullbright.min_light": "Мин. свет",
|
"setting.fullbright.min_light": "Мин. свет",
|
||||||
"setting.seeinvisibles.alpha": "Альфа",
|
|
||||||
"setting.soundesp.name_mode": "Режим имени",
|
"setting.soundesp.name_mode": "Режим имени",
|
||||||
"setting.soundesp.name_mode.option.Technical": "Технический",
|
"setting.soundesp.name_mode.option.Technical": "Технический",
|
||||||
"setting.soundesp.name_mode.option.Localized": "Локализованный",
|
"setting.soundesp.name_mode.option.Localized": "Локализованный",
|
||||||
|
|
@ -768,20 +767,6 @@
|
||||||
"setting.crosshair.orbiz_move_multiplier": "Множитель движения",
|
"setting.crosshair.orbiz_move_multiplier": "Множитель движения",
|
||||||
"setting.crosshair.orbiz_motion_limit": "Лимит движения",
|
"setting.crosshair.orbiz_motion_limit": "Лимит движения",
|
||||||
"setting.crosshair.orbiz_motion_smoothing": "Сглаживание движения",
|
"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.only_self": "Только себя",
|
||||||
"setting.trails.hide_first_person": "Скрывать в первом лице",
|
"setting.trails.hide_first_person": "Скрывать в первом лице",
|
||||||
"setting.trails.depth_test": "Глубинный тест",
|
"setting.trails.depth_test": "Глубинный тест",
|
||||||
|
|
@ -1103,22 +1088,9 @@
|
||||||
"setting.blockhighlight.breaking_animation.option.static": "Статичная",
|
"setting.blockhighlight.breaking_animation.option.static": "Статичная",
|
||||||
"setting.blockhighlight.breaking_color": "Цвет ломания",
|
"setting.blockhighlight.breaking_color": "Цвет ломания",
|
||||||
"setting.blockhighlight.breaking_color2": "Второй цвет ломания",
|
"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.aspectratio.aspect_multiplier": "Множитель соотношения сторон",
|
||||||
"setting.blockesp.search_blocks": "Искомые блоки",
|
"setting.invisesp.color": "Цвет",
|
||||||
"setting.blockesp.visible_color": "Цвет видимых",
|
"setting.invisesp.line_width": "Толщина линии",
|
||||||
"setting.blockesp.chunk_color": "Цвет чанка",
|
|
||||||
"setting.blockesp.cluster_tracers": "Кластерные трейсеры",
|
|
||||||
"setting.holeesp.range_xz": "Радиус XZ",
|
"setting.holeesp.range_xz": "Радиус XZ",
|
||||||
"setting.holeesp.range_y": "Радиус Y",
|
"setting.holeesp.range_y": "Радиус Y",
|
||||||
"setting.holeesp.height": "Высота",
|
"setting.holeesp.height": "Высота",
|
||||||
|
|
@ -1733,6 +1705,7 @@
|
||||||
"setting.draggable.triangulator.triangulator_cache_lifetime": "Время жизни кеша",
|
"setting.draggable.triangulator.triangulator_cache_lifetime": "Время жизни кеша",
|
||||||
"setting.draggable.triangulator.triangulator_auto_reset": "Автосброс",
|
"setting.draggable.triangulator.triangulator_auto_reset": "Автосброс",
|
||||||
"module.autobed.description": "Автоматически ставит и взрывает кровати в измерениях, где они взрываются.",
|
"module.autobed.description": "Автоматически ставит и взрывает кровати в измерениях, где они взрываются.",
|
||||||
|
"module.invisesp.description": "Обводит игроков, скрытых невидимостью.",
|
||||||
"module.holeesp.description": "Подсвечивает безопасные холки с фейдом по дистанции.",
|
"module.holeesp.description": "Подсвечивает безопасные холки с фейдом по дистанции.",
|
||||||
"setting.autobed.place": "Ставить",
|
"setting.autobed.place": "Ставить",
|
||||||
"setting.autobed.explode": "Взрывать",
|
"setting.autobed.explode": "Взрывать",
|
||||||
|
|
@ -3011,5 +2984,19 @@
|
||||||
"command.toggle.description": "Включает, отключает или переключает модуль.",
|
"command.toggle.description": "Включает, отключает или переключает модуль.",
|
||||||
"command.tps.description": "Показывает расчётное количество серверных тиков в секунду.",
|
"command.tps.description": "Показывает расчётное количество серверных тиков в секунду.",
|
||||||
"command.username.description": "Показывает текущее имя Minecraft и UUID.",
|
"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.TranslatableTextContentMixin",
|
||||||
"security.TranslationStorageMixin",
|
"security.TranslationStorageMixin",
|
||||||
"sodium.SodiumBlockRendererMixin",
|
"sodium.SodiumBlockRendererMixin",
|
||||||
"sodium.SodiumBlockVisibilityMixin",
|
|
||||||
"sodium.SodiumChunkMeshFormatsMixin",
|
"sodium.SodiumChunkMeshFormatsMixin",
|
||||||
"sodium.SodiumChunkBuilderMeshingTaskMixin",
|
|
||||||
"sodium.SodiumChunkVertexMixin",
|
"sodium.SodiumChunkVertexMixin",
|
||||||
"sodium.SodiumShaderChunkRendererMixin",
|
"sodium.SodiumShaderChunkRendererMixin",
|
||||||
"sodium.SodiumVertexConsumerTrackerMixin",
|
"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