chore(history): squash 16 commit(s) from 2026-09-09

- docs: Cosmetics Overhaul design spec (slot-based cosmetics + built-in head/back modules)
- chore: drop design doc, implementing directly from TODO.md
- feat: Cosmetics module overhaul — 8 body-zone slots + 3D preview screen
- feat: AlwaysVisibleHand module + KineticLyrics text scale & timing offset
- feat: KillEffect Portal/Tornado procedural shader modes
- feat: KineticLyrics sync — millisecond position prediction (playerctl-style)
- feat: SkyShader Nebula mode, BlockHighlight shader outlines, crosshairs
- feat: CombatLog module — damage dealt/taken log with DPS stats
- refactor(8.5.1): regroup mixins/ 108 files into domain leaf packages (<=4 each) + regenerate mixins.json
- refactor(8.5.1): split accessors into 12 groups, iris/sodium/security into leaves, fix plugin FQNs + mixins.json
- refactor(8.5.1): split visuals/ 67 files into esp/nametags/hats/effects/weather/scene/camera/player/items/fx packages; lyrics->lrc; member visibility fixes; configs untouched (module ids preserved)
- refactor(8.5.1): split features/gui/clickgui/settings 51 files into 18 leaf packages (core/color/slider/text/bind/...); visibility promotion only, no behavior changes
- refactor(8.5.1): split features/gui/hud/draggable/impl 40 files into stats/metrics/status/combat/panels/targethud/scripted leaves; TargetHudModel records nested; visibility-only
- refactor(8.5.1): split events tree (impl 32 -> 8 domains, top -> core/meta); TODO 8.5 progress notes
- refactor(8.5.1): split features/gui/chat 31 files into model/render/text/input/suggestions/menu/messages leaves
- fix(8.5.1 audit): delete dead unregistered ItemRenderStateLayerAccessor, drop ghost sodium FQNs from plugin, remove vestigial visuals.* star import
This commit is contained in:
loki5512344 2026-09-09 23:55:33 +02:00
parent 707820e418
commit fb6e6eb1d3
690 changed files with 5923 additions and 2774 deletions

View file

@ -241,3 +241,161 @@
- [ ] Не смешивать с Фазой 7: разбивка структуры — отдельными проходами, иначе diff-каша
### Проверка: `./gradlew test` зелёный
## Фаза 8.5: Аудит структуры (правила ≤200 строк / ≤4 файла) — ПОЛНЫЙ СПИСОК НА РЕФАКТОРИНГ
> Снимок 2026-09-09 (`src/main/java`, 1459 файлов / 197k строк).
> **Гейт перед Фазой 9.2 (Optimize)**: движковые файлы, которые Optimize будет трогать,
> должны быть разбиты заранее. 9.1 (мёртвые грузы) от этого НЕ зависит — идёт своим порядком.
> Стратегия та же, что Фаза 3/8: фасад + package-private хелперы ≤200, 0 правок вызывающих,
> билд после каждого файла. Фасад-исключение (Renderer2D) — см. оговорку ниже.
### Итоговая картина
- **Файлы >200 строк: 244 шт.** → >800 строк: 26 | 400–800: 78 | 201–400: 140
- **Папки >4 файлов: 86 шт.**
- Разбивка больших файлов по зонам: features/gui — 73 | render/engine — 54 |
features/module — 35 | util — ~35 | mixins — 3 | config — ~6 | прочее — остальное
### 8.5.1 Папки >4 файлов — топ должников (разбить на подпапки по смыслу)
- [x] `mixins/` — 108 файлов → leaf-пакеты (entity/blaze3d/input/world/network/gui/hud/render),
mixins.json перегенерирован, FQN-строки LoVisualMixinPlugin обновлены. Коммиты 94d5af7, 3c76817.
- [x] `mixins/accessors/` — 45 → 12 групп (entity/renderer/world/state/packet/inventory/...). Коммит 3c76817.
Также разбиты iris/sodium/security на подпакеты.
- [x] `features/module/modules/visuals/` — 67 → esp/nametags/hats/effects/weather/scene/camera/
player/items/fx + lyrics/lrc. Конфиг-id НЕ менялись (проверено). Коммит 073ac26.
- [x] `features/gui/clickgui/settings/` — 51 → core/bool/mode/slider/text/text-list/group/bind/
color(+render/preset/hex)/i18n/render/skin/owner/cooldown/util. Коммит 9db1fec.
- [x] `features/gui/hud/draggable/impl/` — 40 → stats/metrics/status/combat/panels/targethud(+/render,
render/parts)/scripted + hudnotifier/model, targethud-модель свёрнута в класс с record-ами. Коммит 6b89e40.
- [x] `events/impl/` — 32 → input/movement/world/network/entity/pvp/render/player;
events/ верхний уровень: core(Event,EventBus,Events) + meta(EventHandler,UsedImplicitly).
- [ ] `features/gui/chat/` — 31 → `render/`, `suggestions/`, `menu/`, `model/`.
- [ ] `render/engine/uniform/impl/` — 28 → по потребителям (`uniform/impl/pipeline/`, `uniform/impl/effects/`).
- [ ] `render/engine/renderer/ui/` — 28: верхний уровень разнести по уже существующим
`draw/`, `runtime/`, добавить `blur/`, `batches/`.
- [ ] `render/engine/text/` — 24 и `text/backend/` — 12 → `text/` разбить на `text/render/`,
`text/layout/`, `text/msdf/`.
- [ ] `render/engine/renderer/ui/runtime/script/` — 23, `ui/draw/` — 23 → внутри по 4-файловым кластерам.
- [ ] `features/gui/hud/nondraggable/impl/` — 20 (+`impl/tab/`) → `impl/overlays/`, `impl/tablist/`.
- [ ] `features/gui/clickgui/` — 20 на верхнем уровне → всё в существующие `layout/`, `sections/`,
`picker/`, `protocol/`, `util/`.
- [ ] `render/engine/command/` — 17, `render/helpers/` — 16, `features/command/impl/` — 16,
`config/values/` — 16 → по 2-3 подпапки (типам: `values/numeric/`, `values/color/`…).
- [ ] `render/engine/profiler/` — 14, `modules/player/` — 14, `features/gui/hud/` — 14,
`addon/` — 14 — средние: раскидать при касании.
- [ ] Хвост (5–13 файлов, ~55 папок): `util/pvp` 13, `util/projectile` 13, `render/iris` 13,
`config/profile` 13, `features/module/misc` 12, `features/module` 12, `config` 12 и т.д. —
НЕ выделенный проход, а «касаться = чинить» (см. 8.5.4).
### 8.5.2 Файлы-гиганты >600 строк — приоритетная очередь разбивки (53 файла)
> Формат: файл (строк) → идея разбивки. Порядок сверху вниз. Facade-оговорка:
> Renderer2D остаётся фасадом-делегатом (правило Фазы 8), но его вспомогательные
> классы (kawase/blur/glass) — вынести из OrderedUiBatcher (935) в `ui/blur/`.
- [ ] Renderer2D 2166 — ФАСАД (исключение по правилу Фазы 8): оставить делегирование,
но блоки >200 строк переносить в package-private хелперы в `renderer/renderer2d/`
(частично уже есть Rects2D/Strokes2D)
- [ ] LoVisualAltManagerScreen 1113 — экран+список+диалоги → 4 файла `mainmenu/alt/`
- [ ] UiPrimitiveRenderer 1042 — по типам примитивов (rect/round/line/text-batch)
- [ ] BetterButtons 1032 — extraction+tooltip+render → `hud/nondraggable/impl/buttons/`
- [ ] InventorySwap 1031 — по стратегиям свапа; чистая логика отдельными классами с тестами
- [ ] SvgMeshBackend 1014 — parser/triangulate/cache разнести
- [ ] Themes 1012 — реестр тем в `theme/presets/` (по 4 темы/файл), дефолты отдельно
- [ ] WorldParticles 991 — типы частиц в `visuals/effects/particles/`
- [ ] Module 989 — базовый класс: settings-механику вынести в `module/ext/` (осторожно, общий предок!)
- [ ] LoVisualAddonManagerScreen 989 — как AltManager
- [ ] Chams 976 — режимы рендера в подклассы-политики (фасад модуля тонкий)
- [ ] ClickGuiPickerState 962 + CombatProtocolHeuristicsEditorState 932 — state/actions хелперы
- [ ] ModulesMenuScreen 960 — рендер категорий отдельно от состояния
- [ ] UiMeshGeometry 954 — по примитивам геометрии
- [ ] BetterTooltips 953, OrderedUiBatcher 935 (→ batches + blur), Scoreboard 924,
Rain 893 (+CompactHudStatModel 893), TazikHat 859 (обещан в Фазе 7 — разбить при портировании),
ShaderPatchCompiler 843, OnlineRelationPlayerPickerComponent 837, CustomTabList 830,
GameRendererMixin 810 (только хуки; тело вынести в `render/engine/entry/`-классы —
ВНИМАНИЕ: 9.1 тоже его трогает, координировать), Predictions 805
- [ ] Хвост 400–800 (~78 файлов, см. /tmp-снимок в истории команд: ConfigProfilesComponent 791,
HitEffect 770, PlayerMovementSimulation 755, ProjectilePuncher 746, MediaPlayer 739,
Crosshair 737, MeshBuilder 729, SettingsPanelComponent 703, Admins 702, Cooldowns 701,
Itemizer 699, BlockHighlight 692, LocalPlayerMixin 687, Trails 681, ItemBatchRenderer 673,
CosmeticModel 671, MenuScreen 667, GlStencilFramebufferSupport 663, ConfigSerializer 663,
DynamicIsland 661, ReimaginedVisual 660, RelationsComponent 659, UiStyle 640, Potions 617,
ModuleComponent 612, MsdfFont 606, Keybinds 603, MediaSessionService 594 и др.)
### 8.5.3 201–400 строк (~140 файлов) — проход «касаться = чинить»
- [ ] Не делать отдельным марафоном: при правке файла по другой фазе — первым коммитом
разбивка на ≤200, вторым — целевое изменение (иначе diff-каша, правило Фазы 8)
### 8.5.4 Порядок и критерии готовности
- [ ] Очередь: 8.5.1 крупные папки (mixins, visuals, clickgui/settings, hud impl) →
8.5.2 гиганты >800 → далее по убыванию. Каждая папка/файл = отдельный коммит.
- [ ] После каждого шага: `./gradlew build` ✓, `./gradlew test` ✓; конфиг-id модулей и
ключи сеттингов НЕ ломаем (совместимость .lvcfg); скрипты-переименования пакетов
не трогают string-literals id — проверять grep'ом после moves.
- [ ] Критерий закрытия для старта 9.2: ноль файлов >200 в путях, которые трогает Optimize
(Renderer2D-секции blur/glass, OrderedUiBatcher, HUD-имплементации bgEffect,
MsdfFont/CustomTextRenderer), и ноль папок >4 в этих же пакетах. Полный ноль по всем
244/86 — долгий хвост, 9.2 им не блокируется.
- [ ] Обновить этот раздел при подходе к волне (отмечать [x], доснимать счётчики).
## Фаза 9: Оптимизация FPS (аудит 2026-09-09) — ПЛАН
> Диагноз: у клиента есть «налог каждый кадр», который платится даже при
> выключенных модулях (друг: просадка FPS на чистом конфиге, Windows).
> Решение владельца: мёртвые грузы — ЧИНИМ в коде; авто-деградации по FPS НЕ ДЕЛАЕМ;
> вместо неё — ручной модуль **Optimize** (включил → порезали стекло/blur/тяжёлые проходы).
### 9.1 Мёртвые грузы (чиним в коде всегда, независимо от модулей) — приоритет
- [ ] **glGetError на каждом render pass** — `GlStencilShapeClipBackend.endRenderPass`
(`:140`) и `beginRenderPass` (`:98/:104/:110/:124`) дёргают `checkGlErrors`
(`:428-434`) на КАЖДЫЙ проход (миксин `CommandEncoderMixin:102-112` висит на
submitRenderPass → это и все ванильные/Sodium проходы). `glGetError` = синк с
Windows-драйвером, несколько мс/кадр. Фикс: проверять только если
`mode != DISABLED || renderPassAttachmentRequired`, либо под dev-флагом;
в релизе — no-op.
- [ ] **`lovisual$isVulkanBackend()` на каждый submitRenderPass**
(`CommandEncoderMixin:66-74`) — `getDeviceInfo().backendName().toLowerCase()`
заново на кадр×проходы. Фикс: вычислить один раз, закэшировать boolean.
- [ ] **PreTranslucentDepth — полноэкранный depth-copy каждый кадр без нужды** —
`LevelRendererMixin:275-283` гейтится только «нет Iris». Фикс: гейт
`needsPreTranslucentDepthCapture()` по образцу `WorldSceneDepth`
(`LevelRendererMixin:160-218`); потребители: `PostProcessGraphResources:61`,
`LoVisualVisuals:79`, `WorldPassCompiler:174`.
- [ ] **copyMainColor каждый кадр ДО гейта** — `UiBlurResources.captureWorldSource`
(`:116-123`): полноэкранный color-copy делается до проверки
`liquidGlassBlurRequested` (`:122`). Фикс: переставить — copy только когда
blur реально запрошен.
- [ ] **Строки профайлера при выключенном профайлере** — конкатенации на call-site
каждый кадр: `PostProcessGraph:59` (`"graph:" + phase`), `ModuleManager:219-221/:290`
(`"3d:module:" + m.name()`). Фикс: ленивые supplier'ы / ранний isEnabled-check.
- [ ] **Мелочь, по ходу**: `new PoseStack`/`new Matrix4f`×2/`new Frustum`×2 в кадре
(`GameRendererMixin:203-239/:341-347/:609-617`) — закэшить переиспользуемые копии.
### 9.2 Модуль **Optimize** (misc) — ручной рубильник экономии
> ⛔ ГЕЙТ: не начинать, пока не закрыта Фаза 8.5 (рефакторинг-аудитStructure ниже) —
> Optimize трогает Renderer2D/OrderedUiBatcher/HUD-имплементации, все они >200 строк.
> Идея владельца: один тумблер «хочу максимум FPS». НИКАКОГО автовключения по FPS.
> Пока активен — движок и HUD работают в лёгком режиме.
- [ ] Каркас: `modules/misc/Optimize.java` (≤200 строк, тест `OptimizeTest` на чистую логику).
Настройки (bool, дефолт on при включении модуля):
- `no_glass` — все glass/blur-эффекты HUD off: форсить bgEffect=None у всех
HUD-элементов (Fps сейчас по умолчанию "Blur", `Fps:70`), сделать
`Renderer2D.requestLiquidGlassBlur*` no-op, пропускать kawase-цепочку в
`OrderedUiBatcher.prepareSharedBlurInternal`.
- `lite_post` — резать тапы тяжёлых пост-проходов: MotionBlur 15→5
(+ neighborhood 10→0), DoF 32→8 — через uniform, без переписывания шейдеров
(альтернатива жёстче: временно деактивировать проходы — решить на реализации, KISS).
- `lite_sky` — процедурные скайбоксы (reimagined_skybox / pulsar / thunder,
~20 fbm-оценок на пиксель) — пропускать noise-слои.
- `lean_hud` — off для per-frame аллокационных прибамбасов: градиенты/бегущие
строки (`CustomTextRenderer:387` new String на глиф), анимации цифр.
- [ ] Механизм «как движок узнаёт»: НЕ тыкать статику модуля в лоб — сделать
`OptimizeState` (boolean + getter, обновление при enable/disable), читать через
него в движке (паттерн уже есть: `ReimaginedVisual.needsWorldSceneDepthCapture()`).
- [ ] Взаимодействие с 9.1: фиксы 9.1 НЕ отменяются модулем — copy/ошибки чиним всегда;
Optimize режет только то, что имеет визуальную цену.
- [ ] i18n en_us + ru_ru обязательно: `module.optimize.*`, `setting.optimize.*`.
- [ ] Нотификация при первом включении (chat/HUD): что именно порезано.
### 9.3 Проверка
- [ ] A/B-мерка до/после 9.1 на чистом конфиге (F3/DebugHud, пустой мир, 1080p):
целевой эффект — минус 2 полноэкранных copy/кадр и минус N glGetError-синков
(N = проходов за кадр).
- [ ] `./gradlew build` после каждого пункта; `./gradlew test` зелёный.
- [ ] Порядок: 9.1 (по одному пункту на коммит) → 9.2.

View file

@ -0,0 +1,3 @@
{
"enabled": false
}

View file

@ -7,7 +7,7 @@
package dev.loki.lovisual;
import dev.loki.lovisual.events.impl.GameTickEvent;
import dev.loki.lovisual.events.impl.render.GameTickEvent;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
@ -25,7 +25,7 @@ import dev.loki.lovisual.events.Events;
import dev.loki.lovisual.features.account.AccountConfig;
import dev.loki.lovisual.features.command.CommandManager;
import dev.loki.lovisual.features.gui.clickgui.ClickGuiRenderer;
import dev.loki.lovisual.features.gui.clickgui.settings.I18nPreflightContributors;
import dev.loki.lovisual.features.gui.clickgui.settings.i18n.I18nPreflightContributors;
import dev.loki.lovisual.features.gui.hud.HudElements;
import dev.loki.lovisual.features.gui.hud.HudRenderSpace;
import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry;
@ -72,6 +72,7 @@ import dev.loki.lovisual.util.pvp.PvpTargetTracker;
import dev.loki.lovisual.util.pvp.PvpTracker;
import dev.loki.lovisual.util.target.TargetManager;
import dev.loki.lovisual.util.time.TimerController;
import dev.loki.lovisual.features.gui.clickgui.settings.bind.KeyBindSetting;
public class LoVisual implements ClientModInitializer {

View file

@ -9,7 +9,7 @@ package dev.loki.lovisual.addon;
import java.util.List;
record AddonDescriptor(
public record AddonDescriptor(
String id,
String name,
String version,

View file

@ -15,7 +15,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
final class AddonRegistration {
public final class AddonRegistration {
final AddonDescriptor descriptor;
final EntrypointContainer<LoVisualAddon> entrypoint;
final List<String> modules = new ArrayList<>();

View file

@ -20,7 +20,7 @@ import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
final class AddonStateStore {
public final class AddonStateStore {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private AddonStateStore() {

View file

@ -30,7 +30,7 @@ import net.fabricmc.loader.api.ModContainer;
import java.nio.file.Path;
final class LoVisualAddonContextImpl implements LoVisualAddonContext {
public final class LoVisualAddonContextImpl implements LoVisualAddonContext {
private final AddonRegistration registration;
LoVisualAddonContextImpl(AddonRegistration registration) {

View file

@ -10,7 +10,7 @@ package dev.loki.lovisual.addon;
import dev.loki.lovisual.api.v0.addon.LoVisualAddonRuntimeContext;
import dev.loki.lovisual.api.v0.client.LoVisualClientApi;
final class LoVisualAddonRuntimeContextImpl implements LoVisualAddonRuntimeContext {
public final class LoVisualAddonRuntimeContextImpl implements LoVisualAddonRuntimeContext {
private final String addonId;
LoVisualAddonRuntimeContextImpl(String addonId) {

View file

@ -13,7 +13,7 @@ import dev.loki.lovisual.features.module.ModuleAutoLoader;
import java.util.ArrayList;
import java.util.List;
final class LoVisualModuleLoadContextImpl implements LoVisualModuleLoadContext {
public final class LoVisualModuleLoadContextImpl implements LoVisualModuleLoadContext {
private final AddonRegistration registration;
private final List<String> pendingExclusions = new ArrayList<>();

View file

@ -13,7 +13,7 @@ import dev.loki.lovisual.config.values.ConfigValue;
import dev.loki.lovisual.config.values.ModeValue;
import dev.loki.lovisual.features.module.Module;
final class ModuleExtensionContextImpl implements ModuleExtensionContext {
public final class ModuleExtensionContextImpl implements ModuleExtensionContext {
private final String addonId;
private final Module module;

View file

@ -7,7 +7,7 @@
package dev.loki.lovisual.api.v0.addon;
import dev.loki.lovisual.events.UsedImplicitly;
import dev.loki.lovisual.events.meta.UsedImplicitly;
import dev.loki.lovisual.features.command.ClientCommand;
import dev.loki.lovisual.api.v0.clickgui.LoVisualClickGuiSection;
import dev.loki.lovisual.features.gui.hud.AbstractHudElement;

View file

@ -7,7 +7,7 @@
package dev.loki.lovisual.api.v0.addon;
import dev.loki.lovisual.events.UsedImplicitly;
import dev.loki.lovisual.events.meta.UsedImplicitly;
import dev.loki.lovisual.features.module.Module;
/**

View file

@ -24,7 +24,7 @@ import xaero.hud.path.XaeroPath;
import java.util.ArrayList;
import java.util.List;
final class XaeroWaypointRuntime {
public final class XaeroWaypointRuntime {
private XaeroWaypointRuntime() {
}

View file

@ -7,7 +7,6 @@
package dev.loki.lovisual.config;
import dev.loki.lovisual.config.values.*;
import dev.loki.lovisual.config.values.*;
import dev.loki.lovisual.render.iris.IrisRuntime;
import dev.loki.lovisual.util.logging.DebugLog;

View file

@ -9,7 +9,7 @@ package dev.loki.lovisual.config;
import dev.loki.lovisual.config.common.CommonSettingSchema;
import dev.loki.lovisual.config.values.*;
import dev.loki.lovisual.features.gui.clickgui.settings.TextListSetting;
import dev.loki.lovisual.features.gui.clickgui.settings.text.list.TextListSetting;
import java.lang.reflect.Field;
import java.util.ArrayList;

View file

@ -8,6 +8,7 @@
package dev.loki.lovisual.config.common;
import dev.loki.lovisual.config.common.impl.*;
import dev.loki.lovisual.features.module.modules.visuals.esp.ESP;
public enum CommonSettingSchemas {
;

View file

@ -16,7 +16,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
enum ConfigProfileValueCodec {
public enum ConfigProfileValueCodec {
;
private static final byte NULL = 0;
private static final byte BOOLEAN = 1;

View file

@ -19,6 +19,8 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import dev.loki.lovisual.events.Event;
import dev.loki.lovisual.events.meta.EventHandler;
public final class EventBus {
private static final Subscriber[] EMPTY_SUBSCRIBERS = new Subscriber[0];

View file

@ -6,6 +6,7 @@
*/
package dev.loki.lovisual.events;
import dev.loki.lovisual.events.EventBus;
public enum Events {
;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.entity;
import lombok.Getter;
import net.minecraft.world.entity.Entity;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.entity;
import lombok.Getter;
import lombok.Setter;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.entity;
import net.minecraft.world.entity.LivingEntity;
import dev.loki.lovisual.events.Event;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.entity;
import lombok.Getter;
import lombok.Setter;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.input;
import lombok.Getter;
import net.minecraft.client.input.KeyEvent;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.input;
import lombok.Getter;
import lombok.Setter;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.input;
import dev.loki.lovisual.events.Event;
import lombok.Getter;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.input;
import dev.loki.lovisual.events.Event;
import lombok.Getter;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.movement;
import lombok.Getter;
import lombok.Setter;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.movement;
import dev.loki.lovisual.events.Event;
import lombok.Getter;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.movement;
import dev.loki.lovisual.events.Event;
import lombok.Getter;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.movement;
import lombok.Getter;
import net.minecraft.world.phys.Vec3;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.network;
import lombok.Getter;
import net.minecraft.network.protocol.Packet;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.network;
import dev.loki.lovisual.events.Event;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.network;
import dev.loki.lovisual.events.Event;
import lombok.Getter;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.network;
import lombok.Getter;
import net.minecraft.network.protocol.Packet;

View file

@ -5,13 +5,13 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.player;
import dev.loki.lovisual.config.SettingDef;
import dev.loki.lovisual.config.SettingOwner;
import dev.loki.lovisual.events.Event;
import dev.loki.lovisual.features.gui.clickgui.settings.Setting;
import dev.loki.lovisual.features.gui.clickgui.settings.SettingFactory;
import dev.loki.lovisual.features.gui.clickgui.settings.core.Setting;
import dev.loki.lovisual.features.gui.clickgui.settings.core.SettingFactory;
import java.util.ArrayList;
import java.util.Collections;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.player;
import dev.loki.lovisual.events.Event;
import lombok.Getter;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.player;
import lombok.Getter;
import lombok.Setter;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.player;
import dev.loki.lovisual.events.Event;
import lombok.Getter;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.pvp;
import dev.loki.lovisual.events.Event;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.pvp;
import dev.loki.lovisual.events.Event;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.pvp;
import net.minecraft.network.chat.Component;
import dev.loki.lovisual.events.Event;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.pvp;
import dev.loki.lovisual.events.Event;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.render;
import dev.loki.lovisual.events.Event;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.render;
import dev.loki.lovisual.events.Event;
import lombok.Getter;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.render;
import lombok.Getter;
import net.minecraft.client.renderer.state.LightmapRenderState;
@ -13,6 +13,7 @@ import net.minecraft.util.Mth;
import org.joml.Vector3f;
import org.joml.Vector3fc;
import dev.loki.lovisual.events.Event;
import dev.loki.lovisual.features.module.modules.visuals.player.FullBright;
public class LightmapModifyEvent extends Event {
private static final float EPSILON = 1.0e-4f;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.render;
import dev.loki.lovisual.events.Event;
import dev.loki.lovisual.render.engine.svg.SvgRegistry;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.world;
import lombok.Getter;
import net.minecraft.core.BlockPos;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.world;
import lombok.Getter;
import lombok.Setter;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.world;
import dev.loki.lovisual.events.Event;
import lombok.Getter;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events.impl;
package dev.loki.lovisual.events.impl.world;
import lombok.Getter;
import lombok.Setter;

View file

@ -5,12 +5,13 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events;
package dev.loki.lovisual.events.meta;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import dev.loki.lovisual.events.meta.UsedImplicitly;
@UsedImplicitly
@Retention(RetentionPolicy.RUNTIME)

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.events;
package dev.loki.lovisual.events.meta;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;

View file

@ -7,7 +7,7 @@
package dev.loki.lovisual.features.command;
import dev.loki.lovisual.events.UsedImplicitly;
import dev.loki.lovisual.events.meta.UsedImplicitly;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;

View file

@ -17,7 +17,7 @@ import java.util.Comparator;
import java.util.List;
import java.util.Locale;
final class CommandUtils {
public final class CommandUtils {
private CommandUtils() {
}

View file

@ -15,6 +15,7 @@ import dev.loki.lovisual.features.module.Module;
import java.util.List;
import java.util.Locale;
import dev.loki.lovisual.features.gui.hud.draggable.impl.panels.ModuleList;
@CommandInfo(
id = "hide",

View file

@ -12,6 +12,7 @@ import dev.loki.lovisual.features.command.CommandContext;
import dev.loki.lovisual.features.command.CommandInfo;
import dev.loki.lovisual.features.command.CommandOutput;
import dev.loki.lovisual.util.player.NetworkStatsUtil;
import dev.loki.lovisual.features.gui.hud.draggable.impl.stats.Ping;
@CommandInfo(
id = "ping",

View file

@ -15,6 +15,7 @@ import dev.loki.lovisual.util.player.NetworkStatsUtil;
import net.minecraft.client.multiplayer.ServerData;
import java.util.Locale;
import dev.loki.lovisual.features.gui.hud.draggable.impl.stats.Ping;
@CommandInfo(
id = "serverinfo",

View file

@ -2,54 +2,89 @@ package dev.loki.lovisual.features.cosmetic;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Axis;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.visuals.cosmetics.CosmeticDisplay;
public final class CosmeticAttachmentTransforms {
private CosmeticAttachmentTransforms() {
}
public static void apply(PoseStack matrices, CosmeticType type, CosmeticModel model) {
if (type == CosmeticType.PET) {
// TODO: replace with CosmeticAnimator.getPetHoverOffset() once the animator is ported.
float bob = 0f;
matrices.mulPose(Axis.YP.rotationDegrees(180f));
matrices.translate(0.45f, 2.15f + bob, 0.12f);
matrices.scale(0.55f, 0.55f, 0.55f);
return;
/**
* Base attachment transform for a slot, then the user's per-slot offset/rotation/scale
* override from {@link CosmeticDisplay} (accessory slots only).
*/
public static void apply(PoseStack matrices, CosmeticSlot slot, CosmeticModel model) {
applyBase(matrices, slot);
if (slot.isAccessory()) {
applyUserOverride(matrices, slot);
}
if (type == CosmeticType.ACCESSORY && model != null) {
String folder = model.getFolderName().toLowerCase();
if (folder.contains("witch") || folder.contains("hat")) {
}
private static void applyUserOverride(PoseStack matrices, CosmeticSlot slot) {
CosmeticDisplay module = Modules.get(CosmeticDisplay.class);
if (module == null) return;
CosmeticDisplay.SlotOffsetSettings offsets = module.offsetsFor(slot);
if (offsets == null) return;
matrices.translate(offsets.offsetX().get(), offsets.offsetY().get(), offsets.offsetZ().get());
matrices.mulPose(Axis.YP.rotationDegrees(offsets.rotation().get()));
float scale = offsets.scale().get();
matrices.scale(scale, scale, scale);
}
private static void applyBase(PoseStack matrices, CosmeticSlot slot) {
switch (slot) {
case PET -> {
// TODO: replace with CosmeticAnimator.getPetHoverOffset() once the animator is ported.
float bob = 0f;
matrices.mulPose(Axis.YP.rotationDegrees(180f));
matrices.translate(0.45f, 2.15f + bob, 0.12f);
matrices.scale(0.55f, 0.55f, 0.55f);
}
case HEAD -> {
matrices.translate(0f, 1.72f, 0f);
matrices.scale(0.52f, 0.52f, 0.52f);
} else if (folder.contains("wing")) {
}
case SHOULDERS -> {
matrices.translate(0f, 1.45f, 0f);
matrices.scale(0.5f, 0.5f, 0.5f);
}
case BACK -> {
matrices.translate(0f, 0.1f, -0.3f);
matrices.mulPose(Axis.YP.rotationDegrees(180f));
matrices.scale(0.85f, 0.85f, 0.85f);
} else if (folder.contains("wheel")) {
}
case WAIST -> {
matrices.translate(0f, 0.55f, 0.25f);
matrices.scale(0.55f, 0.55f, 0.55f);
} else {
}
case LEGS -> {
matrices.translate(0f, 0.3f, 0f);
matrices.scale(0.5f, 0.5f, 0.5f);
}
case HELD -> {
matrices.translate(0f, 1.3f, 0f);
matrices.scale(0.5f, 0.5f, 0.5f);
}
case MODEL -> {
// no base transform: full-body model replaces the player 1:1
}
}
}
public static void applyPreview(PoseStack matrices, CosmeticModel model) {
if (model == null) return;
String folder = model.getFolderName().toLowerCase();
if (model.getType() == CosmeticType.PET) {
CosmeticSlot slot = model.getSlot();
if (slot == CosmeticSlot.PET) {
matrices.translate(6f, 22f, 1f);
matrices.scale(1.8f, 1.8f, 1.8f);
} else if (model.getType() == CosmeticType.ACCESSORY) {
if (folder.contains("witch") || folder.contains("hat")) {
matrices.translate(0f, 20f, 0f);
} else if (folder.contains("wing")) {
matrices.translate(0f, 2f, -4f);
matrices.scale(1.3f, 1.3f, 1.3f);
} else if (folder.contains("wheel")) {
matrices.translate(0f, 6f, 3f);
}
} else if (slot == CosmeticSlot.HEAD) {
matrices.translate(0f, 20f, 0f);
} else if (slot == CosmeticSlot.BACK) {
matrices.translate(0f, 2f, -4f);
matrices.scale(1.3f, 1.3f, 1.3f);
} else if (slot == CosmeticSlot.WAIST) {
matrices.translate(0f, 6f, 3f);
}
}
}

View file

@ -1,15 +1,18 @@
package dev.loki.lovisual.features.cosmetic;
import java.util.EnumMap;
import java.util.Map;
/**
* Holds which cosmetic (model/pet/accessory) the local user currently has equipped.
* Holds which cosmetic the local user currently has equipped, per slot.
* MODEL/PET are single-active slots; the six accessory slots (HEAD/SHOULDERS/
* BACK/WAIST/LEGS/HELD) are independent and can all be active at once.
* Rendering and animation live in separate classes (not part of this core pass).
*/
public final class CosmeticManager {
private static final CosmeticManager INSTANCE = new CosmeticManager();
private CosmeticModel activeModel;
private CosmeticModel activePet;
private CosmeticModel activeAccessory;
private final Map<CosmeticSlot, CosmeticModel> active = new EnumMap<>(CosmeticSlot.class);
private CosmeticManager() {
}
@ -23,52 +26,55 @@ public final class CosmeticManager {
* (by id) against the freshly loaded models.
*/
public void init() {
String modelId = activeModel != null ? activeModel.getId() : null;
String petId = activePet != null ? activePet.getId() : null;
String accessoryId = activeAccessory != null ? activeAccessory.getId() : null;
Map<CosmeticSlot, String> equippedIds = new EnumMap<>(CosmeticSlot.class);
for (Map.Entry<CosmeticSlot, CosmeticModel> entry : active.entrySet()) {
if (entry.getValue() != null) {
equippedIds.put(entry.getKey(), entry.getValue().getId());
}
}
for (CosmeticModel model : CosmeticRepository.get().all()) {
model.resetTexture();
}
CosmeticRepository.get().reload();
active.clear();
CosmeticRepository repo = CosmeticRepository.get();
activeModel = modelId != null ? repo.get(modelId) : null;
activePet = petId != null ? repo.get(petId) : null;
activeAccessory = accessoryId != null ? repo.get(accessoryId) : null;
for (Map.Entry<CosmeticSlot, String> entry : equippedIds.entrySet()) {
CosmeticModel resolved = repo.get(entry.getValue());
if (resolved != null) {
active.put(entry.getKey(), resolved);
}
}
}
public CosmeticModel getActive(CosmeticType type) {
return switch (type) {
case PET -> activePet;
case ACCESSORY -> activeAccessory;
case MODEL -> activeModel;
};
public CosmeticModel getActive(CosmeticSlot slot) {
return active.get(slot);
}
public void setActive(CosmeticType type, CosmeticModel model) {
public void setActive(CosmeticSlot slot, CosmeticModel model) {
if (model != null) {
CosmeticModel resolved = CosmeticRepository.get().get(model.getId());
if (resolved != null) {
model = resolved;
}
}
switch (type) {
case PET -> activePet = model;
case ACCESSORY -> activeAccessory = model;
case MODEL -> activeModel = model;
if (model == null) {
active.remove(slot);
} else {
active.put(slot, model);
}
}
public void clear(CosmeticType type) {
setActive(type, null);
public void clear(CosmeticSlot slot) {
setActive(slot, null);
}
public boolean hasAnyActive() {
return activeModel != null || activePet != null || activeAccessory != null;
return !active.isEmpty();
}
public boolean isActive() {
return activeModel != null;
return active.get(CosmeticSlot.MODEL) != null;
}
}

View file

@ -40,6 +40,7 @@ public class CosmeticModel {
private final String accentColor;
private final String folderName;
private final CosmeticType type;
private final CosmeticSlot slot;
private final File externalDirectory;
@Setter private String source = "Своя";
@ -63,18 +64,27 @@ public class CosmeticModel {
private CosmeticTextureSlot fallbackSlot;
private CosmeticModel(String id, String name, String description, String accentColor,
String folderName, CosmeticType type, File externalDirectory) {
String folderName, CosmeticType type, CosmeticSlot slot, File externalDirectory) {
this.id = id;
this.name = name;
this.description = description;
this.accentColor = accentColor;
this.folderName = folderName;
this.type = type;
this.slot = slot != null ? slot : defaultSlotFor(type, folderName);
this.externalDirectory = externalDirectory;
}
private static CosmeticSlot defaultSlotFor(CosmeticType type, String folderName) {
return switch (type) {
case MODEL -> CosmeticSlot.MODEL;
case PET -> CosmeticSlot.PET;
case ACCESSORY -> CosmeticSlot.guessFromFolderName(folderName);
};
}
public CosmeticModel withType(CosmeticType newType) {
CosmeticModel copy = new CosmeticModel(id, name, description, accentColor, folderName, newType, externalDirectory);
CosmeticModel copy = new CosmeticModel(id, name, description, accentColor, folderName, newType, null, externalDirectory);
copy.rootBones.addAll(rootBones);
copy.renderRoots.addAll(renderRoots);
copy.boneMap.putAll(boneMap);
@ -141,7 +151,8 @@ public class CosmeticModel {
: avatar.has("type") ? avatar.get("type").getAsString() : null
);
CosmeticModel model = new CosmeticModel(catalogId, name, desc, color, folderName, type, externalDir);
CosmeticSlot slot = avatar.has("slot") ? CosmeticSlot.fromJson(avatar.get("slot").getAsString()) : null;
CosmeticModel model = new CosmeticModel(catalogId, name, desc, color, folderName, type, slot, externalDir);
InputStream modelStream = openModelStream(externalDir, folderName);
if (modelStream == null) {
@ -392,6 +403,7 @@ public class CosmeticModel {
parent.getAccentColor(),
petFolder,
CosmeticType.PET,
null,
null
);
pet.source = parent.source;

View file

@ -30,10 +30,10 @@ public final class CosmeticRenderer {
private CosmeticRenderer() {
}
public static void renderOnPlayer(Renderer3D renderer, Player player, CosmeticType type, float tickDelta) {
public static void renderOnPlayer(Renderer3D renderer, Player player, CosmeticSlot slot, float tickDelta) {
if (renderer == null || player == null) return;
CosmeticModel model = CosmeticManager.get().getActive(type);
CosmeticModel model = CosmeticManager.get().getActive(slot);
if (model == null) return;
model.loadTexture();
@ -45,7 +45,7 @@ public final class CosmeticRenderer {
PoseStack matrices = new PoseStack();
matrices.translate(pos.x, pos.y, pos.z);
CosmeticAttachmentTransforms.apply(matrices, type, model);
CosmeticAttachmentTransforms.apply(matrices, slot, model);
matrices.mulPose(Axis.YP.rotationDegrees(180f - bodyYaw));
Map<Integer, List<PendingFace>> byTexture = new HashMap<>();
@ -58,14 +58,14 @@ public final class CosmeticRenderer {
Vec3 cam = RenderState.cameraPos;
for (Map.Entry<Integer, List<PendingFace>> entry : byTexture.entrySet()) {
CosmeticTextureSlot slot = model.getTextureSlot(entry.getKey());
if (slot == null) continue;
if (!slot.isLoaded()) {
model.ensureTextureSlotLoaded(slot);
CosmeticTextureSlot textureSlot = model.getTextureSlot(entry.getKey());
if (textureSlot == null) continue;
if (!textureSlot.isLoaded()) {
model.ensureTextureSlotLoaded(textureSlot);
}
if (!slot.isLoaded()) continue;
if (!textureSlot.isLoaded()) continue;
AbstractTexture texture = mc.getTextureManager().getTexture(slot.getId());
AbstractTexture texture = mc.getTextureManager().getTexture(textureSlot.getId());
if (texture == null || texture.getTextureView() == null || texture.getSampler() == null) continue;
Renderer3D.BatchBindings bindings = Renderer3D.BatchBindings.none()

View file

@ -42,6 +42,10 @@ public final class CosmeticRepository {
return catalog.values().stream().filter(m -> m.getType() == type).toList();
}
public List<CosmeticModel> bySlot(CosmeticSlot slot) {
return catalog.values().stream().filter(m -> m.getSlot() == slot).toList();
}
public CosmeticModel get(String id) {
return catalog.get(id);
}

View file

@ -0,0 +1,62 @@
package dev.loki.lovisual.features.cosmetic;
/**
* Where a cosmetic is worn. MODEL/PET are whole-body/companion slots (one active
* item each); the remaining six are independent accessory slots that can all be
* active simultaneously (e.g. a hat + wings + a belt at once).
*/
public enum CosmeticSlot {
MODEL("model", "Модель"),
PET("pet", "Питомец"),
HEAD("head", "Голова"),
SHOULDERS("shoulders", "Плечи"),
BACK("back", "Спина"),
WAIST("waist", "Пояс"),
LEGS("legs", "Ноги"),
HELD("held", "В руке");
private final String id;
private final String displayName;
CosmeticSlot(String id, String displayName) {
this.id = id;
this.displayName = displayName;
}
public String getId() {
return id;
}
public String getDisplayName() {
return displayName;
}
public boolean isAccessory() {
return this != MODEL && this != PET;
}
public static CosmeticSlot fromJson(String raw) {
if (raw == null || raw.isBlank()) return null;
return switch (raw.trim().toLowerCase()) {
case "model", "модель" -> MODEL;
case "pet", "pets", "питомец", "питомцы" -> PET;
case "head", "hat", "голова", "шляпа" -> HEAD;
case "shoulders", "shoulder", "плечи" -> SHOULDERS;
case "back", "спина" -> BACK;
case "waist", "belt", "пояс" -> WAIST;
case "legs", "leg", "ноги" -> LEGS;
case "held", "hand", "рука" -> HELD;
default -> null;
};
}
/** Best-effort slot guess from the model's folder name (legacy accessories without a "slot" key). */
public static CosmeticSlot guessFromFolderName(String folderName) {
if (folderName == null) return HELD;
String folder = folderName.toLowerCase();
if (folder.contains("witch") || folder.contains("hat")) return HEAD;
if (folder.contains("wing")) return BACK;
if (folder.contains("wheel")) return WAIST;
return HELD;
}
}

View file

@ -1,85 +0,0 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
import net.minecraft.network.chat.HoverEvent;
import net.minecraft.network.chat.Style;
import java.util.List;
/**
* Shared package-private records extracted from {@link BetterChatRenderer}.
* The only public-facing record kept nested on the facade is {@link BetterChatRenderer.Layout}.
* Records that own non-trivial rendering logic ({@code FrameLayout}, {@code ContextMenu}) live in
* their own files ({@link BetterChatFrameModel}, {@link BetterChatContextMenuModel}).
*/
final class BetterChatModel {
private BetterChatModel() {
}
record MousePos(double rawX, double rawY, double fx, double fy) {
}
record InputLine(int start, int end, float width) {
}
record Segment(String text, Style style) {
}
record VisualLine(ChatLine message, int messageIndex, int messageGroup, int startChar, int endChar, List<Glyph> glyphs) {
}
record Glyph(
float x0,
float x1,
int charIndex,
int charEndExclusive,
int color,
HoverEvent hover,
String font,
String text,
Style style
) {
}
record GlyphBox(
float x0,
float y0,
float x1,
float y1,
int charIndex,
int charEndExclusive,
int color,
String font,
HoverEvent hover,
String text,
Style style
) {
}
record CachedLine(int startChar, int endChar, List<Glyph> glyphs) {
}
record CachedMessageLayout(float fontSize, float maxWidth, List<CachedLine> lines) {
}
record FrameLine(ChatLine message, int messageIndex, int messageGroup, float y0, float y1, List<GlyphBox> glyphs) {
}
record MessageBubble(ChatLine message, int messageGroup, float x, float y, float w, float h, FrameLine firstLine, FrameLine lastLine) {
}
record MessageClipBounds(float x, float y, float w, float h) {
}
record PickResult(FrameLine line, GlyphBox glyph) {
}
record LayoutResult(List<VisualLine> lines, int messagesSeen) {
}
}

View file

@ -1,211 +0,0 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* 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.CommandOutput;
import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat;
import dev.loki.lovisual.render.engine.color.RenderColor;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.text.TextRenderer;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.gui.screens.ChatScreen;
import net.minecraft.network.chat.Style;
import net.minecraft.util.Mth;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import dev.loki.lovisual.util.text.TextSelection;
/**
* Facade for the BetterChat HUD renderer. This enum singleton is now a thin delegation point:
* it owns the shared per-frame state and constants read/written by the package-private helpers,
* and re-exports the original public API exactly as before so no caller changes.
*/
public enum BetterChatRenderer {
;
// ---- Constants (package-private; read by helpers) ----
static final int MAX_MESSAGE_CHARS = 256;
static final float PADDING = 8f;
static final float LINE_SPACING = 3f;
static final float RADIUS = 24f;
static final float MESSAGE_PAD_X = 8f;
static final float MESSAGE_PAD_Y = 5f;
static final float MESSAGE_GAP = 4f;
static final float MESSAGE_INPUT_GAP = 8f;
static final float MESSAGE_RADIUS = 9f;
static final float MESSAGE_TS_GAP = 8f;
static final float INPUT_RADIUS = 12f;
static final float SEARCH_ICON_WIDTH = 34f;
static final float SEARCH_ICON_HEIGHT = 22f;
static final float NEW_MESSAGE_REVEAL_SECONDS = 0.34f;
static final float NEW_MESSAGE_SLIDE_PX = 18f;
static final int MAX_HISTORY = 32000;
static final float CHAT_TOOLTIP_OFFSET_X = 6f;
static final float CHAT_TOOLTIP_OFFSET_Y = 12f;
static final double DRAG_SELECT_THRESHOLD = 4.0;
static final RenderColor TMP_COLOR = new RenderColor(0xFFFFFFFF);
// ---- Layout caches (package-private) ----
static final IdentityHashMap<ChatLine, BetterChatModel.CachedMessageLayout> MESSAGE_LAYOUT_CACHE = new IdentityHashMap<>();
static final IdentityHashMap<ChatLine, List<BetterChatModel.Segment>> SEGMENT_CACHE = new IdentityHashMap<>();
static final IdentityHashMap<CommandOutput.MessageLine, ChatLine> COMMAND_LINE_CACHE = new IdentityHashMap<>();
static final TextSelection selection = new TextSelection();
// ---- Persistent UI state (package-private) ----
static Renderer2D renderer;
static float lastRenderWidth = 0f;
static float lastRenderHeight = 0f;
static int scrollOffsetLines = 0;
static float smoothScrollOffsetLines = 0f;
static int maxScrollLines = 0;
static float lastFontSizeForUi = 16f;
static List<ChatLine> lastMessages = Collections.emptyList();
static boolean draggingScrollbar = false;
static float sbTrackX, sbTrackY, sbTrackW, sbTrackH;
static float sbThumbY, sbThumbH;
static float sbDragOffset = 0f;
static long lastScrollInteractionMs = 0L;
static boolean sbEnabled = false;
static int sbTotalLines = 0;
static int sbVisibleLines = 0;
static float inputBoxX = 0f, inputBoxY = 0f, inputBoxW = 0f, inputBoxH = 0f;
static boolean hasInputBox = false;
static boolean suggestActive = false;
static float suggestX, suggestY, suggestW, suggestH, suggestItemH;
static float suggestRowsX, suggestRowsY, suggestRowsW, suggestRowsH;
static int suggestStart, suggestVisible, suggestTotal;
static net.minecraft.client.gui.components.CommandSuggestions.SuggestionsList suggestWindow;
static List<Suggestion> suggestEntries = Collections.emptyList();
static boolean lastHoverWasOutsideSuggest = true;
static boolean suggestHasScrollbar = false;
static boolean suggestDraggingScrollbar = false;
static float suggestTrackX, suggestTrackY, suggestTrackW, suggestTrackH, suggestThumbH;
static int suggestCurrentStart = -1;
static int suggestCurrentSelection = -1;
static double lastMouseFx = 0.0;
static double lastMouseFy = 0.0;
static boolean lastMouseValid = false;
static boolean tooltipCaptured = false;
static String lastSuggestInput = "";
static int lastSuggestCursor = -1;
static int lastSuggestSelStart = -1;
static int lastSuggestSelEnd = -1;
static CommandSuggestorBridge.SuggestionSnapshot lastSnapshot = null;
static long debugPerfWindowStartNs = 0L;
static long debugPerfAccumLayoutNs = 0L;
static long debugPerfAccumBuildNs = 0L;
static int debugPerfAccumFrames = 0;
static BetterChatFrameModel.FrameLayout frame = BetterChatFrameModel.FrameLayout.empty();
static BetterChatContextMenuModel.ContextMenu contextMenu = BetterChatContextMenuModel.ContextMenu.closed();
static boolean selecting = false;
static boolean searchHit = false;
static float searchRectX = 0f, searchRectY = 0f, searchRectW = 0f, searchRectH = 0f;
static boolean searchHotkeyDown = false;
static float chatHitX = 0f, chatHitY = 0f, chatHitW = 0f, chatHitH = 0f;
static String hoverEntityUuid = null;
static ChatScreen storedScreen;
static BetterChatStore previewStore;
static int lastSuggestionHash = 0;
static boolean leftPending = false;
static double leftDownFx = 0.0, leftDownFy = 0.0;
static int leftDownMsgIndex = -1;
static int leftDownCharIndex = -1;
static Style leftDownStyle = null;
// ---- Per-frame transient context (package-private; populated by the phased helpers) ----
static GuiGraphicsExtractor ctxGui;
static float ctxBaseX, ctxBaseY;
static BetterChat ctxSettings;
static net.minecraft.client.Minecraft ctxMc;
static boolean ctxPreview, ctxChatOpen, ctxActiveSurface;
static boolean ctxShowTs, ctxHasSeconds, ctxTipDate, ctxTipUnix, ctxAutoFollow;
static int ctxTsColor, ctxTotalMessages, ctxVisibleLimit, ctxMessagesSeen;
static float ctxWidth, ctxContentWidth, ctxTsWidth;
static float ctxLineHeight, ctxBoxHeight, ctxLineYOffset, ctxTargetVisible;
static float ctxStableBoxHeight, ctxFixedInputY, ctxFrameY;
static int[] ctxMessageGroups;
static List<BetterChatModel.VisualLine> ctxAllLines;
static List<BetterChatModel.VisualLine> ctxVisible;
static long ctxPerfT0, ctxPerfT1;
static ChatHoverUtil.HoverTip ctxHoverTip;
// ---- Public API (delegating) ----
public static void markLayoutDirty() {
BetterChatLayoutEngine.markLayoutDirty();
}
public static void renderEngine(Renderer2D rendererIn,
TextRenderer fallback,
GuiGraphicsExtractor ctx,
int screenW,
float baseX,
float baseY) {
BetterChatFrameRenderer.renderEngine(rendererIn, fallback, ctx, screenW, baseX, baseY);
}
public static void renderCapturedTooltip(GuiGraphicsExtractor ctx) {
BetterChatTooltipRenderer.renderCapturedTooltip(ctx);
}
public static boolean onScroll(double delta) {
return BetterChatInputHandler.onScroll(delta);
}
public static boolean onMouseButton(double mx, double my, int button, boolean pressed) {
return BetterChatMouseClickHandler.onMouseButton(mx, my, button, pressed);
}
public static void onMouseMove(double mx, double my) {
BetterChatInputHandler.onMouseMove(mx, my);
}
public static void copySelectionToClipboard() {
BetterChatContextMenuHandler.copySelectionToClipboard();
}
public static void resetScroll() {
BetterChatInputHandler.resetScroll();
}
public static void applyAutoPrefixIfNeeded(EditBox field) {
// Compact BetterChat no longer owns chat-prefix chips; hook kept for compatibility.
}
public static void setSuggestionContext(ChatScreen screen, GuiGraphicsExtractor context, int mouseX, int mouseY) {
storedScreen = screen;
}
public static void onNewMessage() {
BetterChatInputHandler.onNewMessage();
}
public static float getLastWidth() {
return lastRenderWidth;
}
public static float getLastHeight() {
return lastRenderHeight;
}
public static boolean contains(float mx, float my) {
return BetterChatInputHandler.contains(mx, my);
}
public static boolean isInteractive(float mx, float my) {
return BetterChatInputHandler.isInteractive(mx, my);
}
public record Layout(float x, float y) {
}
}

View file

@ -12,13 +12,17 @@ import net.minecraft.util.Mth;
import java.util.Collections;
import java.util.List;
import dev.loki.lovisual.features.gui.chat.model.BetterChatModel;
import dev.loki.lovisual.features.gui.chat.model.ChatLine;
import dev.loki.lovisual.features.gui.chat.render.BetterChatFrameModel;
import dev.loki.lovisual.features.gui.chat.render.BetterChatRenderer;
/**
* Second render phase: turns the laid-out visual lines into a viewport + frame layout
* (bubbles/lines) and animates smooth scrolling. Writes the resulting {@link BetterChatFrameModel.FrameLayout}
* back onto the facade and populates the per-frame sizing context used by every later phase.
*/
final class BetterChatViewport {
public final class BetterChatViewport {
private BetterChatViewport() {
}
@ -67,7 +71,7 @@ final class BetterChatViewport {
return safe;
}
static void composeFrame() {
public static void composeFrame() {
List<BetterChatModel.VisualLine> allLines = BetterChatRenderer.ctxAllLines;
BetterChatModel.LayoutResult layoutResult = new BetterChatModel.LayoutResult(allLines, BetterChatRenderer.ctxMessagesSeen);
List<ChatLine> messages = BetterChatRenderer.lastMessages;

View file

@ -5,25 +5,25 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
package dev.loki.lovisual.features.gui.chat.input;
import dev.loki.lovisual.mixins.accessors.TextFieldWidgetAccessor;
import dev.loki.lovisual.mixins.accessors.widget.TextFieldWidgetAccessor;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.util.Mth;
/**
* EditBox selection introspection + word-boundary helper, independent of the chat renderer.
*/
final class BetterChatEditBoxUtil {
public final class BetterChatEditBoxUtil {
private BetterChatEditBoxUtil() {
}
static int selectionStart(EditBox field, int fallback) {
public static int selectionStart(EditBox field, int fallback) {
if (field == null) return fallback;
return Mth.clamp(field.getCursorPosition(), 0, field.getValue().length());
}
static int selectionEnd(EditBox field, int fallback) {
public static int selectionEnd(EditBox field, int fallback) {
if (field == null) return fallback;
try {
return Mth.clamp(((TextFieldWidgetAccessor) field).lovisual$getSelectionEnd(), 0, field.getValue().length());
@ -35,12 +35,12 @@ final class BetterChatEditBoxUtil {
}
}
static void setCursorSafe(EditBox field, int pos) {
public static void setCursorSafe(EditBox field, int pos) {
if (field == null) return;
field.moveCursorTo(Mth.clamp(pos, 0, field.getValue().length()), false);
}
static String wordAt(String text, int charIndex) {
public static String wordAt(String text, int charIndex) {
if (text == null || text.isEmpty()) return "";
int idx = Mth.clamp(charIndex, 0, Math.max(0, text.length() - 1));
int start = idx;

View file

@ -5,23 +5,28 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
package dev.loki.lovisual.features.gui.chat.input;
import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat;
import dev.loki.lovisual.util.screen.ClientScreen;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.ChatScreen;
import org.lwjgl.glfw.GLFW;
import dev.loki.lovisual.features.gui.chat.input.BetterChatMouseClickHandler;
import dev.loki.lovisual.features.gui.chat.model.BetterChatModel;
import dev.loki.lovisual.features.gui.chat.render.BetterChatRenderer;
import dev.loki.lovisual.features.gui.chat.suggestions.BetterChatSuggestionState;
import dev.loki.lovisual.features.gui.chat.text.util.BetterChatDrawUtil;
/**
* Chat-surface interaction: scroll, drag-selection movement, scrollbar drag and hit-testing.
* The mouse-button dispatch lives in {@link BetterChatMouseClickHandler}.
*/
final class BetterChatInputHandler {
public final class BetterChatInputHandler {
private BetterChatInputHandler() {
}
static BetterChatModel.MousePos resolveMouse(double rawX, double rawY) {
public static BetterChatModel.MousePos resolveMouse(double rawX, double rawY) {
return new BetterChatModel.MousePos(rawX, rawY, rawX, rawY);
}
@ -58,7 +63,7 @@ final class BetterChatInputHandler {
return new BetterChatModel.PickResult(bestLine, glyph);
}
static boolean onScroll(double delta) {
public static boolean onScroll(double delta) {
Minecraft mc = Minecraft.getInstance();
if (!BetterChat.isActive()) return false;
if (!(ClientScreen.current() instanceof ChatScreen)) return false;
@ -81,7 +86,7 @@ final class BetterChatInputHandler {
return false;
}
static void onMouseMove(double mx, double my) {
public static void onMouseMove(double mx, double my) {
if (!BetterChat.isActive()) return;
BetterChatModel.MousePos mouse = resolveMouse(mx, my);
double fx = mouse.fx();
@ -116,7 +121,7 @@ final class BetterChatInputHandler {
BetterChatRenderer.selection.updateCaret(pick.line().messageIndex(), selectionCaretIndex(pick.glyph()));
}
static void resetScroll() {
public static void resetScroll() {
BetterChatRenderer.scrollOffsetLines = 0;
BetterChatRenderer.smoothScrollOffsetLines = 0f;
BetterChatRenderer.draggingScrollbar = false;
@ -124,14 +129,14 @@ final class BetterChatInputHandler {
BetterChatRenderer.sbDragOffset = 0f;
}
static void onNewMessage() {
public static void onNewMessage() {
if (BetterChatRenderer.scrollOffsetLines > 0 && BetterChatRenderer.scrollOffsetLines <= 2) {
BetterChatRenderer.scrollOffsetLines = 0;
BetterChatRenderer.smoothScrollOffsetLines = 0f;
}
}
static boolean isInteractive(float mx, float my) {
public static boolean isInteractive(float mx, float my) {
boolean inChat = BetterChatRenderer.chatHitW > 0f && mx >= BetterChatRenderer.chatHitX && mx <= BetterChatRenderer.chatHitX + BetterChatRenderer.chatHitW && my >= BetterChatRenderer.chatHitY && my <= BetterChatRenderer.chatHitY + BetterChatRenderer.chatHitH;
boolean inSearch = BetterChatRenderer.searchHit && mx >= BetterChatRenderer.searchRectX && mx <= BetterChatRenderer.searchRectX + BetterChatRenderer.searchRectW && my >= BetterChatRenderer.searchRectY && my <= BetterChatRenderer.searchRectY + BetterChatRenderer.searchRectH;
boolean inInput = BetterChatRenderer.hasInputBox && mx >= BetterChatRenderer.inputBoxX && mx <= BetterChatRenderer.inputBoxX + BetterChatRenderer.inputBoxW && my >= BetterChatRenderer.inputBoxY && my <= BetterChatRenderer.inputBoxY + BetterChatRenderer.inputBoxH;
@ -141,11 +146,11 @@ final class BetterChatInputHandler {
return inChat || inSearch || inInput || inSuggest || inContext || inScrollbar;
}
static boolean contains(float mx, float my) {
public static boolean contains(float mx, float my) {
return isInteractive(mx, my);
}
static void updateChatHit() {
public static void updateChatHit() {
float hitBottom = BetterChatRenderer.hasInputBox ? BetterChatRenderer.inputBoxY + BetterChatRenderer.inputBoxH : BetterChatRenderer.ctxFixedInputY;
BetterChatRenderer.chatHitX = BetterChatRenderer.ctxBaseX;
BetterChatRenderer.chatHitY = BetterChatRenderer.frame.y();
@ -153,7 +158,7 @@ final class BetterChatInputHandler {
BetterChatRenderer.chatHitH = Math.max(BetterChatRenderer.ctxBoxHeight, hitBottom - BetterChatRenderer.chatHitY);
}
static void clearChatHit() {
public static void clearChatHit() {
BetterChatRenderer.chatHitX = BetterChatRenderer.chatHitY = BetterChatRenderer.chatHitW = BetterChatRenderer.chatHitH = 0f;
BetterChatRenderer.lastMouseValid = false;
}

View file

@ -4,8 +4,8 @@
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
import dev.loki.lovisual.mixins.accessors.ChatScreenAccessor;
package dev.loki.lovisual.features.gui.chat.input;
import dev.loki.lovisual.mixins.accessors.chat.ChatScreenAccessor;
import dev.loki.lovisual.render.engine.text.FontInfo;
import dev.loki.lovisual.render.engine.text.Fonts;
import dev.loki.lovisual.render.engine.text.TextRenderer;
@ -17,13 +17,19 @@ import net.minecraft.client.gui.screens.ChatScreen;
import java.util.ArrayList;
import java.util.List;
import static dev.loki.lovisual.features.theme.Theme.theme;
import dev.loki.lovisual.features.gui.chat.input.BetterChatEditBoxUtil;
import dev.loki.lovisual.features.gui.chat.messages.BetterChatSearch;
import dev.loki.lovisual.features.gui.chat.model.BetterChatModel;
import dev.loki.lovisual.features.gui.chat.render.BetterChatRenderer;
import dev.loki.lovisual.features.gui.chat.text.util.BetterChatDrawUtil;
import dev.loki.lovisual.features.gui.chat.text.util.BetterChatFontUtil;
/**
* Input-field rendering: wrapping, caret, selection highlight, char counter and search chip.
*/
final class BetterChatInputRenderer {
public final class BetterChatInputRenderer {
private BetterChatInputRenderer() {
}
static void renderInput(Minecraft mc, float fontSize, float boxW, float x, float inputY) {
public static void renderInput(Minecraft mc, float fontSize, float boxW, float x, float inputY) {
if (!(ClientScreen.current() instanceof ChatScreen chatScreen)) return;
EditBox field = ((ChatScreenAccessor) chatScreen).getChatField();
if (field == null) return;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
package dev.loki.lovisual.features.gui.chat.input;
import com.mojang.blaze3d.platform.InputConstants;
import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat;
@ -14,16 +14,24 @@ import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.ChatScreen;
import net.minecraft.util.Mth;
import org.lwjgl.glfw.GLFW;
import dev.loki.lovisual.features.gui.chat.input.BetterChatInputHandler;
import dev.loki.lovisual.features.gui.chat.menu.BetterChatContextMenuHandler;
import dev.loki.lovisual.features.gui.chat.menu.BetterChatContextMenuModel;
import dev.loki.lovisual.features.gui.chat.messages.BetterChatSearch;
import dev.loki.lovisual.features.gui.chat.model.BetterChatModel;
import dev.loki.lovisual.features.gui.chat.render.BetterChatRenderer;
import dev.loki.lovisual.features.gui.chat.suggestions.BetterChatSuggestionState;
import dev.loki.lovisual.features.gui.chat.text.util.BetterChatDrawUtil;
/**
* Mouse-button dispatch over the chat surface: scrollbar drag, suggest rows, search icon,
* context menu and click/selection logic. Delegates textual actions to {@link BetterChatContextMenuHandler}.
*/
final class BetterChatMouseClickHandler {
public final class BetterChatMouseClickHandler {
private BetterChatMouseClickHandler() {
}
static boolean onMouseButton(double mx, double my, int button, boolean pressed) {
public static boolean onMouseButton(double mx, double my, int button, boolean pressed) {
Minecraft mc = Minecraft.getInstance();
if (!BetterChat.isActive()) return false;
if (!(ClientScreen.current() instanceof ChatScreen)) return false;

View file

@ -4,11 +4,11 @@
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
package dev.loki.lovisual.features.gui.chat.menu;
import dev.loki.lovisual.features.command.CommandManager;
import dev.loki.lovisual.features.command.CommandPrefix;
import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat;
import dev.loki.lovisual.mixins.accessors.ChatScreenAccessor;
import dev.loki.lovisual.mixins.accessors.chat.ChatScreenAccessor;
import dev.loki.lovisual.util.screen.ClientScreen;
import dev.loki.lovisual.util.text.ChatNameUtil;
import dev.loki.lovisual.util.text.ClipboardUtil;
@ -22,13 +22,20 @@ import net.minecraft.util.Mth;
import net.minecraft.util.Util;
import java.util.ArrayList;
import java.util.List;
import dev.loki.lovisual.features.gui.chat.input.BetterChatEditBoxUtil;
import dev.loki.lovisual.features.gui.chat.menu.BetterChatContextMenuModel;
import dev.loki.lovisual.features.gui.chat.menu.BetterChatTooltipRenderer;
import dev.loki.lovisual.features.gui.chat.model.BetterChatModel;
import dev.loki.lovisual.features.gui.chat.model.ChatLine;
import dev.loki.lovisual.features.gui.chat.render.BetterChatRenderer;
import dev.loki.lovisual.features.gui.chat.text.util.BetterChatFontUtil;
/**
* Right-click context menu construction, click-event handling and chat prefill actions.
*/
final class BetterChatContextMenuHandler {
public final class BetterChatContextMenuHandler {
private BetterChatContextMenuHandler() {
}
static void openContextMenu(BetterChatModel.PickResult pick, double mx, double my) {
public static void openContextMenu(BetterChatModel.PickResult pick, double mx, double my) {
List<BetterChatContextMenuModel.ContextMenu.MenuEntry> entries = new ArrayList<>();
ChatLine msg = pick.line().message();
String full = msg.text().getString();
@ -69,7 +76,7 @@ final class BetterChatContextMenuHandler {
}
BetterChatRenderer.contextMenu = BetterChatContextMenuModel.ContextMenu.open((float) mx, (float) my, entries);
}
static void copySelectionToClipboard() {
public static void copySelectionToClipboard() {
String selected = selectedText();
if (selected.isEmpty()) return;
ClipboardUtil.copy(selected);
@ -98,7 +105,7 @@ final class BetterChatContextMenuHandler {
}
return out.toString();
}
static boolean handleClickEvent(net.minecraft.network.chat.Style style, boolean ctrlDown) {
public static boolean handleClickEvent(net.minecraft.network.chat.Style style, boolean ctrlDown) {
if (style == null) return false;
ClickEvent evt = style.getClickEvent();
if (evt == null) return false;
@ -142,7 +149,7 @@ final class BetterChatContextMenuHandler {
return false;
}
}
static boolean tryPrefillTellFromClick(int msgIndex, int charIndex) {
public static boolean tryPrefillTellFromClick(int msgIndex, int charIndex) {
if (msgIndex < 0 || msgIndex >= BetterChatRenderer.lastMessages.size()) return false;
String full = BetterChatRenderer.lastMessages.get(msgIndex).text().getString();
List<String> nickCandidates = ChatNameUtil.extractNicks(full);

View file

@ -5,27 +5,30 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
package dev.loki.lovisual.features.gui.chat.menu;
import java.util.Collections;
import java.util.List;
import static dev.loki.lovisual.features.theme.Theme.theme;
import dev.loki.lovisual.features.gui.chat.render.BetterChatRenderer;
import dev.loki.lovisual.features.gui.chat.text.util.BetterChatDrawUtil;
import dev.loki.lovisual.features.gui.chat.text.util.BetterChatFontUtil;
/**
* Right-click context menu record + entry record, extracted from the renderer facade.
* Rendering leans on {@link BetterChatDrawUtil} and {@link BetterChatFontUtil}.
*/
final class BetterChatContextMenuModel {
public final class BetterChatContextMenuModel {
private BetterChatContextMenuModel() {
}
record ContextMenu(boolean open, float x, float y, List<MenuEntry> entries) {
public record ContextMenu(boolean open, float x, float y, List<MenuEntry> entries) {
private static final float MENU_PAD_X = 8f;
private static final float MENU_PAD_Y = 7f;
private static final float MENU_RADIUS = 9f;
static ContextMenu closed() {
public static ContextMenu closed() {
return new ContextMenu(false, 0, 0, Collections.emptyList());
}
@ -37,13 +40,13 @@ final class BetterChatContextMenuModel {
return Math.max(20f, fontSize + 7f);
}
boolean contains(double mx, double my) {
public boolean contains(double mx, double my) {
if (!open) return false;
float fontSize = BetterChatRenderer.lastFontSizeForUi * 0.92f;
return mx >= x && mx <= x + width(fontSize) && my >= y && my <= y + height(fontSize);
}
MenuEntry pick(double my) {
public MenuEntry pick(double my) {
if (!open) return null;
float fontSize = BetterChatRenderer.lastFontSizeForUi * 0.92f;
float itemH = itemHeight(fontSize);
@ -58,7 +61,7 @@ final class BetterChatContextMenuModel {
return null;
}
void render(float mouseX, float mouseY, float fontSize) {
public void render(float mouseX, float mouseY, float fontSize) {
if (!open || entries.isEmpty()) return;
float fs = Math.max(12f, fontSize * 0.92f);
float itemH = itemHeight(fs);
@ -106,7 +109,7 @@ final class BetterChatContextMenuModel {
return MENU_PAD_Y * 2f + entries.size() * itemHeight(fontSize);
}
record MenuEntry(String label, Runnable action) {
public record MenuEntry(String label, Runnable action) {
}
}
}

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
package dev.loki.lovisual.features.gui.chat.menu;
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.BetterTooltips;
import dev.loki.lovisual.util.text.ChatNameUtil;
@ -25,17 +25,22 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static dev.loki.lovisual.features.gui.chat.ChatHoverUtil.HoverTip;
import static dev.loki.lovisual.features.gui.chat.text.util.ChatHoverUtil.HoverTip;
import static dev.loki.lovisual.features.theme.Theme.theme;
import dev.loki.lovisual.features.gui.chat.input.BetterChatEditBoxUtil;
import dev.loki.lovisual.features.gui.chat.model.BetterChatModel;
import dev.loki.lovisual.features.gui.chat.render.BetterChatRenderer;
import dev.loki.lovisual.features.gui.chat.suggestions.BetterChatSuggestionState;
import dev.loki.lovisual.features.gui.chat.text.util.ChatHoverUtil;
/**
* Tooltip capture/render, timestamp formatting and post-pick hover inference.
*/
final class BetterChatTooltipRenderer {
public final class BetterChatTooltipRenderer {
private BetterChatTooltipRenderer() {
}
static String formatTimestamp(long ms, boolean withSeconds) {
public static String formatTimestamp(long ms, boolean withSeconds) {
LocalDateTime dt = LocalDateTime.ofInstant(Instant.ofEpochMilli(ms), ZoneId.systemDefault());
DateTimeFormatter fmt = DateTimeFormatter.ofPattern(withSeconds ? "HH:mm:ss" : "HH:mm");
return dt.format(fmt);
@ -46,7 +51,7 @@ final class BetterChatTooltipRenderer {
return dt.format(DateTimeFormatter.ofPattern("dd.MM.yyyy"));
}
static HoverTip buildTimestampTooltip(long ms, boolean showDate, boolean showUnix) {
public static HoverTip buildTimestampTooltip(long ms, boolean showDate, boolean showUnix) {
List<ChatHoverUtil.ColoredLine> lines = new ArrayList<>();
LocalDateTime dt = LocalDateTime.ofInstant(Instant.ofEpochMilli(ms), ZoneId.systemDefault());
if (showDate) {
@ -62,7 +67,7 @@ final class BetterChatTooltipRenderer {
return new HoverTip(lines, null, false, net.minecraft.world.item.ItemStack.EMPTY);
}
static void captureTooltip(HoverTip tip, float x, float y, GuiGraphicsExtractor ctx) {
public static void captureTooltip(HoverTip tip, float x, float y, GuiGraphicsExtractor ctx) {
if (tip == null || ctx == null) return;
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.getWindow() == null) return;
@ -81,7 +86,7 @@ final class BetterChatTooltipRenderer {
BetterChatRenderer.tooltipCaptured = true;
}
static void renderCapturedTooltip(GuiGraphicsExtractor ctx) {
public static void renderCapturedTooltip(GuiGraphicsExtractor ctx) {
if (!BetterChatRenderer.tooltipCaptured) return;
BetterTooltips.renderTooltipWithContext(ctx);
BetterTooltips.resetTooltipScaleOverride();
@ -103,7 +108,7 @@ final class BetterChatTooltipRenderer {
/**
* Mirror of the original post-pick hover/suggest-inference block (lines 466-518).
*/
static void resolveHover(BetterChatModel.PickResult hover, Minecraft mc) {
public static void resolveHover(BetterChatModel.PickResult hover, Minecraft mc) {
boolean overSuggestWindow = BetterChatSuggestionState.isInsideSuggestWindow(
BetterChatRenderer.lastMouseFx, BetterChatRenderer.lastMouseFy,
BetterChatRenderer.lastMouseFx, BetterChatRenderer.lastMouseFy);

View file

@ -5,19 +5,20 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
package dev.loki.lovisual.features.gui.chat.messages;
import net.minecraft.network.chat.Component;
import java.util.List;
import dev.loki.lovisual.features.gui.chat.model.ChatLine;
final class BetterChatMessageGrouping {
public final class BetterChatMessageGrouping {
private static final String BOX_PREFIXES = "┏┓┗┛┃│║╔╗╚╝╭╮╰╯┌┐└┘┣┫├┤╠╣";
private BetterChatMessageGrouping() {
}
static int[] groupIds(List<ChatLine> messages) {
public static int[] groupIds(List<ChatLine> messages) {
if (messages == null || messages.isEmpty()) {
return new int[0];
}

View file

@ -4,7 +4,7 @@
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
package dev.loki.lovisual.features.gui.chat.messages;
import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat;
import dev.loki.lovisual.features.gui.hud.HudRenderUtil;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
@ -14,15 +14,23 @@ import net.minecraft.util.Mth;
import java.util.List;
import static dev.loki.lovisual.render.engine.text.TextGlyphFallback.isSvgFontKey;
import static dev.loki.lovisual.features.theme.Theme.theme;
import static dev.loki.lovisual.features.gui.chat.ChatHoverUtil.HoverTip;
import static dev.loki.lovisual.features.gui.chat.text.util.ChatHoverUtil.HoverTip;
import dev.loki.lovisual.features.gui.chat.menu.BetterChatTooltipRenderer;
import dev.loki.lovisual.features.gui.chat.model.BetterChatModel;
import dev.loki.lovisual.features.gui.chat.model.ChatLine;
import dev.loki.lovisual.features.gui.chat.render.BetterChatFrameModel;
import dev.loki.lovisual.features.gui.chat.render.BetterChatRenderer;
import dev.loki.lovisual.features.gui.chat.text.BetterChatGlyphRenderer;
import dev.loki.lovisual.features.gui.chat.text.util.BetterChatDrawUtil;
import dev.loki.lovisual.features.gui.chat.text.util.BetterChatFontUtil;
/**
* Message-area drawing: glass bubbles, selection rectangles, timestamp overlays and glyph pickup.
* Extracted from {@link BetterChatRenderer}; behavior unchanged.
*/
final class BetterChatMessageRenderer {
public final class BetterChatMessageRenderer {
private BetterChatMessageRenderer() {
}
static float fade(float ageSeconds) {
public static float fade(float ageSeconds) {
BetterChat cfg = BetterChat.get();
float life = cfg != null ? cfg.fadeSeconds() : 12f;
if (life <= 0f) return 0f;
@ -34,7 +42,7 @@ final class BetterChatMessageRenderer {
float inv = 1f - t;
return inv * inv * inv;
}
static float newMessageReveal(ChatLine message) {
public static float newMessageReveal(ChatLine message) {
if (message == null) return 1f;
float age = message.ageSeconds();
if (age <= 0f) return 0f;
@ -43,7 +51,7 @@ final class BetterChatMessageRenderer {
float inv = 1f - t;
return 1f - inv * inv * inv;
}
static BetterChatModel.PickResult drawPhase() {
public static BetterChatModel.PickResult drawPhase() {
BetterChatFrameModel.FrameLayout frame = BetterChatRenderer.frame;
boolean activeChatSurface = BetterChatRenderer.ctxActiveSurface;
boolean showTs = BetterChatRenderer.ctxShowTs;

View file

@ -5,13 +5,16 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
package dev.loki.lovisual.features.gui.chat.messages;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicLong;
import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat;
import dev.loki.lovisual.features.gui.chat.model.BetterChatStore;
import dev.loki.lovisual.features.gui.chat.model.ChatLine;
/**
* Minimal search state & async search runner for BetterChat, styled like ClickGuiSearch.

View file

@ -5,12 +5,12 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
package dev.loki.lovisual.features.gui.chat.messages;
import com.mojang.brigadier.suggestion.Suggestion;
import dev.loki.lovisual.features.command.CommandManager;
import dev.loki.lovisual.features.command.CommandPrefix;
import dev.loki.lovisual.mixins.accessors.ChatScreenAccessor;
import dev.loki.lovisual.mixins.accessors.chat.ChatScreenAccessor;
import dev.loki.lovisual.util.screen.ClientScreen;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.EditBox;
@ -18,6 +18,9 @@ import net.minecraft.client.gui.screens.ChatScreen;
import net.minecraft.client.input.KeyEvent;
import java.util.List;
import dev.loki.lovisual.features.gui.chat.input.BetterChatEditBoxUtil;
import dev.loki.lovisual.features.gui.chat.render.BetterChatRenderer;
import dev.loki.lovisual.features.gui.chat.suggestions.BetterChatSuggestionState;
/**
* Tab / arrow-key cycling through the custom {@code @}-command suggestion popup.

View file

@ -0,0 +1,89 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat.model;
import net.minecraft.network.chat.HoverEvent;
import net.minecraft.network.chat.Style;
import java.util.List;
import dev.loki.lovisual.features.gui.chat.menu.BetterChatContextMenuModel;
import dev.loki.lovisual.features.gui.chat.model.ChatLine;
import dev.loki.lovisual.features.gui.chat.render.BetterChatFrameModel;
import dev.loki.lovisual.features.gui.chat.render.BetterChatRenderer;
/**
* Shared package-private records extracted from {@link BetterChatRenderer}.
* The only public-facing record kept nested on the facade is {@link BetterChatRenderer.Layout}.
* Records that own non-trivial rendering logic ({@code FrameLayout}, {@code ContextMenu}) live in
* their own files ({@link BetterChatFrameModel}, {@link BetterChatContextMenuModel}).
*/
public final class BetterChatModel {
private BetterChatModel() {
}
public record MousePos(double rawX, double rawY, double fx, double fy) {
}
public record InputLine(int start, int end, float width) {
}
public record Segment(String text, Style style) {
}
public record VisualLine(ChatLine message, int messageIndex, int messageGroup, int startChar, int endChar, List<Glyph> glyphs) {
}
public record Glyph(
float x0,
float x1,
int charIndex,
int charEndExclusive,
int color,
HoverEvent hover,
String font,
String text,
Style style
) {
}
public record GlyphBox(
float x0,
float y0,
float x1,
float y1,
int charIndex,
int charEndExclusive,
int color,
String font,
HoverEvent hover,
String text,
Style style
) {
}
public record CachedLine(int startChar, int endChar, List<Glyph> glyphs) {
}
public record CachedMessageLayout(float fontSize, float maxWidth, List<CachedLine> lines) {
}
public record FrameLine(ChatLine message, int messageIndex, int messageGroup, float y0, float y1, List<GlyphBox> glyphs) {
}
public record MessageBubble(ChatLine message, int messageGroup, float x, float y, float w, float h, FrameLine firstLine, FrameLine lastLine) {
}
public record MessageClipBounds(float x, float y, float w, float h) {
}
public record PickResult(FrameLine line, GlyphBox glyph) {
}
public record LayoutResult(List<VisualLine> lines, int messagesSeen) {
}
}

View file

@ -5,13 +5,14 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
package dev.loki.lovisual.features.gui.chat.model;
import net.minecraft.network.chat.Component;
import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat;
import java.util.ArrayList;
import java.util.List;
import dev.loki.lovisual.features.gui.chat.model.ChatLine;
public final class BetterChatStore {

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
package dev.loki.lovisual.features.gui.chat.model;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@ -30,6 +30,10 @@ import java.io.FileWriter;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import dev.loki.lovisual.features.gui.chat.model.BetterChatStore;
import dev.loki.lovisual.features.gui.chat.model.ChatLine;
import dev.loki.lovisual.features.gui.chat.render.BetterChatRenderer;
import dev.loki.lovisual.features.gui.chat.text.util.BetterChatHoverCache;
/**
* Per-world chat storage + async Gson persistence.

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
package dev.loki.lovisual.features.gui.chat.model;
import net.minecraft.network.chat.Component;

View file

@ -5,28 +5,30 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
package dev.loki.lovisual.features.gui.chat.render;
import net.minecraft.util.Mth;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import dev.loki.lovisual.features.gui.chat.model.BetterChatModel;
import dev.loki.lovisual.features.gui.chat.render.BetterChatRenderer;
/**
* Frame layout record: builds screen-space line/bubble geometry from laid-out visual lines.
* Pure geometry + picking, no rendering or font work.
*/
final class BetterChatFrameModel {
public final class BetterChatFrameModel {
private BetterChatFrameModel() {
}
record FrameLayout(float x, float y, float w, float h, List<BetterChatModel.FrameLine> lines, List<BetterChatModel.MessageBubble> bubbles) {
public record FrameLayout(float x, float y, float w, float h, List<BetterChatModel.FrameLine> lines, List<BetterChatModel.MessageBubble> bubbles) {
static FrameLayout empty() {
return new FrameLayout(0, 0, 0, 0, Collections.emptyList(), Collections.emptyList());
}
static FrameLayout build(List<BetterChatModel.VisualLine> lines, float x, float y, float w, float h, float fontSize, float lineHeight, float yOffset, float timestampReserve) {
public static FrameLayout build(List<BetterChatModel.VisualLine> lines, float x, float y, float w, float h, float fontSize, float lineHeight, float yOffset, float timestampReserve) {
List<BetterChatModel.FrameLine> list = new ArrayList<>(lines.size());
float cursorY = y + BetterChatRenderer.MESSAGE_PAD_Y + yOffset;
int lastMessageGroup = Integer.MIN_VALUE;
@ -90,7 +92,7 @@ final class BetterChatFrameModel {
return bubbles;
}
BetterChatModel.MessageClipBounds messageClipBounds() {
public BetterChatModel.MessageClipBounds messageClipBounds() {
if (lines.isEmpty() || h <= 0f) return null;
float clipX = x + BetterChatRenderer.PADDING;
float clipW = Math.max(1f, w - BetterChatRenderer.PADDING * 2f);
@ -101,7 +103,7 @@ final class BetterChatFrameModel {
return mx >= x && mx <= x + w && my >= y && my <= y + h;
}
BetterChatModel.PickResult pick(double mx, double my) {
public BetterChatModel.PickResult pick(double mx, double my) {
if (!contains(mx, my)) return null;
for (BetterChatModel.FrameLine line : lines) {
if (my < line.y0() || my > line.y1() + BetterChatRenderer.LINE_SPACING) continue;

View file

@ -5,18 +5,30 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
package dev.loki.lovisual.features.gui.chat.render;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.text.TextRenderer;
import dev.loki.lovisual.render.helpers.SystemCursor;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import dev.loki.lovisual.features.gui.chat.BetterChatViewport;
import dev.loki.lovisual.features.gui.chat.input.BetterChatInputHandler;
import dev.loki.lovisual.features.gui.chat.input.BetterChatInputRenderer;
import dev.loki.lovisual.features.gui.chat.menu.BetterChatTooltipRenderer;
import dev.loki.lovisual.features.gui.chat.messages.BetterChatMessageRenderer;
import dev.loki.lovisual.features.gui.chat.messages.BetterChatSearch;
import dev.loki.lovisual.features.gui.chat.model.BetterChatModel;
import dev.loki.lovisual.features.gui.chat.render.BetterChatRenderer;
import dev.loki.lovisual.features.gui.chat.suggestions.BetterChatSuggestionRenderer;
import dev.loki.lovisual.features.gui.chat.suggestions.BetterChatSuggestionState;
import dev.loki.lovisual.features.gui.chat.text.BetterChatLayoutEngine;
import dev.loki.lovisual.features.gui.chat.text.util.BetterChatDrawUtil;
/**
* Single entry point for the chat HUD render pass; orchestrates the layout, message, input,
* suggestion, scrollbar and tooltip phases. All real work lives in the other package-private helpers.
*/
final class BetterChatFrameRenderer {
public final class BetterChatFrameRenderer {
private BetterChatFrameRenderer() {
}

View file

@ -0,0 +1,224 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat.render;
import com.mojang.brigadier.suggestion.Suggestion;
import dev.loki.lovisual.features.command.CommandOutput;
import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat;
import dev.loki.lovisual.render.engine.color.RenderColor;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.text.TextRenderer;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.gui.screens.ChatScreen;
import net.minecraft.network.chat.Style;
import net.minecraft.util.Mth;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import dev.loki.lovisual.util.text.TextSelection;
import dev.loki.lovisual.features.gui.chat.input.BetterChatInputHandler;
import dev.loki.lovisual.features.gui.chat.input.BetterChatMouseClickHandler;
import dev.loki.lovisual.features.gui.chat.menu.BetterChatContextMenuHandler;
import dev.loki.lovisual.features.gui.chat.menu.BetterChatContextMenuModel;
import dev.loki.lovisual.features.gui.chat.menu.BetterChatTooltipRenderer;
import dev.loki.lovisual.features.gui.chat.model.BetterChatModel;
import dev.loki.lovisual.features.gui.chat.model.BetterChatStore;
import dev.loki.lovisual.features.gui.chat.model.ChatLine;
import dev.loki.lovisual.features.gui.chat.render.BetterChatFrameModel;
import dev.loki.lovisual.features.gui.chat.render.BetterChatFrameRenderer;
import dev.loki.lovisual.features.gui.chat.suggestions.CommandSuggestorBridge;
import dev.loki.lovisual.features.gui.chat.text.BetterChatLayoutEngine;
import dev.loki.lovisual.features.gui.chat.text.util.ChatHoverUtil;
/**
* Facade for the BetterChat HUD renderer. This enum singleton is now a thin delegation point:
* it owns the shared per-frame state and constants read/written by the package-private helpers,
* and re-exports the original public API exactly as before so no caller changes.
*/
public enum BetterChatRenderer {
;
// ---- Constants (package-private; read by helpers) ----
public static final int MAX_MESSAGE_CHARS = 256;
public static final float PADDING = 8f;
public static final float LINE_SPACING = 3f;
public static final float RADIUS = 24f;
public static final float MESSAGE_PAD_X = 8f;
public static final float MESSAGE_PAD_Y = 5f;
public static final float MESSAGE_GAP = 4f;
public static final float MESSAGE_INPUT_GAP = 8f;
public static final float MESSAGE_RADIUS = 9f;
public static final float MESSAGE_TS_GAP = 8f;
public static final float INPUT_RADIUS = 12f;
public static final float SEARCH_ICON_WIDTH = 34f;
public static final float SEARCH_ICON_HEIGHT = 22f;
public static final float NEW_MESSAGE_REVEAL_SECONDS = 0.34f;
public static final float NEW_MESSAGE_SLIDE_PX = 18f;
public static final int MAX_HISTORY = 32000;
public static final float CHAT_TOOLTIP_OFFSET_X = 6f;
public static final float CHAT_TOOLTIP_OFFSET_Y = 12f;
public static final double DRAG_SELECT_THRESHOLD = 4.0;
public static final RenderColor TMP_COLOR = new RenderColor(0xFFFFFFFF);
// ---- Layout caches (package-private) ----
public static final IdentityHashMap<ChatLine, BetterChatModel.CachedMessageLayout> MESSAGE_LAYOUT_CACHE = new IdentityHashMap<>();
public static final IdentityHashMap<ChatLine, List<BetterChatModel.Segment>> SEGMENT_CACHE = new IdentityHashMap<>();
public static final IdentityHashMap<CommandOutput.MessageLine, ChatLine> COMMAND_LINE_CACHE = new IdentityHashMap<>();
public static final TextSelection selection = new TextSelection();
// ---- Persistent UI state (package-private) ----
public static Renderer2D renderer;
public static float lastRenderWidth = 0f;
public static float lastRenderHeight = 0f;
public static int scrollOffsetLines = 0;
public static float smoothScrollOffsetLines = 0f;
public static int maxScrollLines = 0;
public static float lastFontSizeForUi = 16f;
public static List<ChatLine> lastMessages = Collections.emptyList();
public static boolean draggingScrollbar = false;
public static float sbTrackX, sbTrackY, sbTrackW, sbTrackH;
public static float sbThumbY, sbThumbH;
public static float sbDragOffset = 0f;
public static long lastScrollInteractionMs = 0L;
public static boolean sbEnabled = false;
public static int sbTotalLines = 0;
public static int sbVisibleLines = 0;
public static float inputBoxX = 0f, inputBoxY = 0f, inputBoxW = 0f, inputBoxH = 0f;
public static boolean hasInputBox = false;
public static boolean suggestActive = false;
public static float suggestX, suggestY, suggestW, suggestH, suggestItemH;
public static float suggestRowsX, suggestRowsY, suggestRowsW, suggestRowsH;
public static int suggestStart, suggestVisible, suggestTotal;
public static net.minecraft.client.gui.components.CommandSuggestions.SuggestionsList suggestWindow;
public static List<Suggestion> suggestEntries = Collections.emptyList();
public static boolean lastHoverWasOutsideSuggest = true;
public static boolean suggestHasScrollbar = false;
public static boolean suggestDraggingScrollbar = false;
public static float suggestTrackX, suggestTrackY, suggestTrackW, suggestTrackH, suggestThumbH;
public static int suggestCurrentStart = -1;
public static int suggestCurrentSelection = -1;
public static double lastMouseFx = 0.0;
public static double lastMouseFy = 0.0;
public static boolean lastMouseValid = false;
public static boolean tooltipCaptured = false;
public static String lastSuggestInput = "";
public static int lastSuggestCursor = -1;
public static int lastSuggestSelStart = -1;
public static int lastSuggestSelEnd = -1;
public static CommandSuggestorBridge.SuggestionSnapshot lastSnapshot = null;
public static long debugPerfWindowStartNs = 0L;
public static long debugPerfAccumLayoutNs = 0L;
public static long debugPerfAccumBuildNs = 0L;
public static int debugPerfAccumFrames = 0;
public static BetterChatFrameModel.FrameLayout frame = BetterChatFrameModel.FrameLayout.empty();
public static BetterChatContextMenuModel.ContextMenu contextMenu = BetterChatContextMenuModel.ContextMenu.closed();
public static boolean selecting = false;
public static boolean searchHit = false;
public static float searchRectX = 0f, searchRectY = 0f, searchRectW = 0f, searchRectH = 0f;
public static boolean searchHotkeyDown = false;
public static float chatHitX = 0f, chatHitY = 0f, chatHitW = 0f, chatHitH = 0f;
public static String hoverEntityUuid = null;
public static ChatScreen storedScreen;
public static BetterChatStore previewStore;
public static int lastSuggestionHash = 0;
public static boolean leftPending = false;
public static double leftDownFx = 0.0, leftDownFy = 0.0;
public static int leftDownMsgIndex = -1;
public static int leftDownCharIndex = -1;
public static Style leftDownStyle = null;
// ---- Per-frame transient context (package-private; populated by the phased helpers) ----
public static GuiGraphicsExtractor ctxGui;
public static float ctxBaseX, ctxBaseY;
public static BetterChat ctxSettings;
public static net.minecraft.client.Minecraft ctxMc;
public static boolean ctxPreview, ctxChatOpen, ctxActiveSurface;
public static boolean ctxShowTs, ctxHasSeconds, ctxTipDate, ctxTipUnix, ctxAutoFollow;
public static int ctxTsColor, ctxTotalMessages, ctxVisibleLimit, ctxMessagesSeen;
public static float ctxWidth, ctxContentWidth, ctxTsWidth;
public static float ctxLineHeight, ctxBoxHeight, ctxLineYOffset, ctxTargetVisible;
public static float ctxStableBoxHeight, ctxFixedInputY, ctxFrameY;
public static int[] ctxMessageGroups;
public static List<BetterChatModel.VisualLine> ctxAllLines;
public static List<BetterChatModel.VisualLine> ctxVisible;
public static long ctxPerfT0, ctxPerfT1;
public static ChatHoverUtil.HoverTip ctxHoverTip;
// ---- Public API (delegating) ----
public static void markLayoutDirty() {
BetterChatLayoutEngine.markLayoutDirty();
}
public static void renderEngine(Renderer2D rendererIn,
TextRenderer fallback,
GuiGraphicsExtractor ctx,
int screenW,
float baseX,
float baseY) {
BetterChatFrameRenderer.renderEngine(rendererIn, fallback, ctx, screenW, baseX, baseY);
}
public static void renderCapturedTooltip(GuiGraphicsExtractor ctx) {
BetterChatTooltipRenderer.renderCapturedTooltip(ctx);
}
public static boolean onScroll(double delta) {
return BetterChatInputHandler.onScroll(delta);
}
public static boolean onMouseButton(double mx, double my, int button, boolean pressed) {
return BetterChatMouseClickHandler.onMouseButton(mx, my, button, pressed);
}
public static void onMouseMove(double mx, double my) {
BetterChatInputHandler.onMouseMove(mx, my);
}
public static void copySelectionToClipboard() {
BetterChatContextMenuHandler.copySelectionToClipboard();
}
public static void resetScroll() {
BetterChatInputHandler.resetScroll();
}
public static void applyAutoPrefixIfNeeded(EditBox field) {
// Compact BetterChat no longer owns chat-prefix chips; hook kept for compatibility.
}
public static void setSuggestionContext(ChatScreen screen, GuiGraphicsExtractor context, int mouseX, int mouseY) {
storedScreen = screen;
}
public static void onNewMessage() {
BetterChatInputHandler.onNewMessage();
}
public static float getLastWidth() {
return lastRenderWidth;
}
public static float getLastHeight() {
return lastRenderHeight;
}
public static boolean contains(float mx, float my) {
return BetterChatInputHandler.contains(mx, my);
}
public static boolean isInteractive(float mx, float my) {
return BetterChatInputHandler.isInteractive(mx, my);
}
public record Layout(float x, float y) {
}
}

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
package dev.loki.lovisual.features.gui.chat.suggestions;
import dev.loki.lovisual.features.command.CommandOutput;
@ -13,16 +13,18 @@ import java.util.ArrayList;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.List;
import dev.loki.lovisual.features.gui.chat.model.ChatLine;
import dev.loki.lovisual.features.gui.chat.render.BetterChatRenderer;
/**
* Merges command-output lines into the visible chat tail, sharing the identity-keyed
* command-line cache held on {@link BetterChatRenderer}.
*/
final class BetterChatCommandMerge {
public final class BetterChatCommandMerge {
private BetterChatCommandMerge() {
}
static List<ChatLine> mergeCommandOutput(List<ChatLine> storeLines,
public static List<ChatLine> mergeCommandOutput(List<ChatLine> storeLines,
List<CommandOutput.MessageLine> commandLines,
int needed) {
if (commandLines == null || commandLines.isEmpty()) {

View file

@ -4,22 +4,28 @@
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
package dev.loki.lovisual.features.gui.chat.suggestions;
import dev.loki.lovisual.features.command.CommandPrefix;
import dev.loki.lovisual.mixins.accessors.ChatScreenAccessor;
import dev.loki.lovisual.mixins.accessors.chat.ChatScreenAccessor;
import dev.loki.lovisual.render.helpers.SystemCursor;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.util.Mth;
import java.util.List;
import static dev.loki.lovisual.features.theme.Theme.theme;
import dev.loki.lovisual.features.gui.chat.input.BetterChatEditBoxUtil;
import dev.loki.lovisual.features.gui.chat.render.BetterChatRenderer;
import dev.loki.lovisual.features.gui.chat.suggestions.BetterChatSuggestionState;
import dev.loki.lovisual.features.gui.chat.suggestions.CommandSuggestorBridge;
import dev.loki.lovisual.features.gui.chat.text.util.BetterChatDrawUtil;
import dev.loki.lovisual.features.gui.chat.text.util.BetterChatFontUtil;
/**
* Draws the autocomplete suggestion popup above the input field each frame.
*/
final class BetterChatSuggestionRenderer {
public final class BetterChatSuggestionRenderer {
private BetterChatSuggestionRenderer() {
}
static void renderSuggestions() {
public static void renderSuggestions() {
BetterChatRenderer.suggestActive = false;
BetterChatRenderer.suggestHasScrollbar = false;
if (BetterChatRenderer.storedScreen == null) return;

View file

@ -5,12 +5,12 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
package dev.loki.lovisual.features.gui.chat.suggestions;
import dev.loki.lovisual.features.command.CommandManager;
import dev.loki.lovisual.features.command.CommandPrefix;
import dev.loki.lovisual.mixins.accessors.ChatScreenAccessor;
import dev.loki.lovisual.mixins.accessors.SuggestionWindowAccessor;
import dev.loki.lovisual.mixins.accessors.chat.ChatScreenAccessor;
import dev.loki.lovisual.mixins.accessors.chat.SuggestionWindowAccessor;
import dev.loki.lovisual.util.screen.ClientScreen;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.EditBox;
@ -24,36 +24,41 @@ import java.util.List;
import static org.lwjgl.glfw.GLFW.GLFW_PRESS;
import static org.lwjgl.glfw.GLFW.glfwGetKey;
import dev.loki.lovisual.features.gui.chat.input.BetterChatEditBoxUtil;
import dev.loki.lovisual.features.gui.chat.messages.BetterChatSearch;
import dev.loki.lovisual.features.gui.chat.render.BetterChatRenderer;
import dev.loki.lovisual.features.gui.chat.suggestions.BetterChatSuggestionRenderer;
import dev.loki.lovisual.features.gui.chat.suggestions.CommandSuggestorBridge;
/**
* Autocomplete suggestion window state: hotkey, snapshot capture, window dragging and hit-test.
* Rendering lives in {@link BetterChatSuggestionRenderer}.
*/
final class BetterChatSuggestionState {
public final class BetterChatSuggestionState {
private BetterChatSuggestionState() {
}
static boolean isInsideSuggestRows(double mx, double my) {
public static boolean isInsideSuggestRows(double mx, double my) {
if (!BetterChatRenderer.suggestActive) return false;
return mx >= BetterChatRenderer.suggestRowsX && mx <= BetterChatRenderer.suggestRowsX + BetterChatRenderer.suggestRowsW
&& my >= BetterChatRenderer.suggestRowsY && my <= BetterChatRenderer.suggestRowsY + BetterChatRenderer.suggestRowsH;
}
static boolean isInsideSuggestRows(double mx, double my, double rawX, double rawY) {
public static boolean isInsideSuggestRows(double mx, double my, double rawX, double rawY) {
return isInsideSuggestRows(mx, my) || isInsideSuggestRows(rawX, rawY);
}
static boolean isInsideSuggestWindow(double mx, double my) {
public static boolean isInsideSuggestWindow(double mx, double my) {
if (!BetterChatRenderer.suggestActive) return false;
return mx >= BetterChatRenderer.suggestX && mx <= BetterChatRenderer.suggestX + BetterChatRenderer.suggestW
&& my >= BetterChatRenderer.suggestY && my <= BetterChatRenderer.suggestY + BetterChatRenderer.suggestH;
}
static boolean isInsideSuggestWindow(double mx, double my, double rawX, double rawY) {
public static boolean isInsideSuggestWindow(double mx, double my, double rawX, double rawY) {
return isInsideSuggestWindow(mx, my) || isInsideSuggestWindow(rawX, rawY);
}
static boolean isInsideSuggestScrollbar(double mx, double my) {
public static boolean isInsideSuggestScrollbar(double mx, double my) {
if (!BetterChatRenderer.suggestActive || !BetterChatRenderer.suggestHasScrollbar) return false;
float hitPad = 4f;
return mx >= BetterChatRenderer.suggestTrackX - hitPad &&
@ -62,7 +67,7 @@ final class BetterChatSuggestionState {
my <= BetterChatRenderer.suggestTrackY + BetterChatRenderer.suggestTrackH + hitPad;
}
static void resetSuggestionTracking() {
public static void resetSuggestionTracking() {
BetterChatRenderer.lastSuggestInput = "";
BetterChatRenderer.lastSuggestCursor = -1;
BetterChatRenderer.lastSuggestSelStart = -1;
@ -76,7 +81,7 @@ final class BetterChatSuggestionState {
BetterChatRenderer.suggestCurrentSelection = -1;
}
static char commandPrefix(String text) {
public static char commandPrefix(String text) {
if (text == null) return 0;
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
@ -101,7 +106,7 @@ final class BetterChatSuggestionState {
return true;
}
static void applySuggestionWindowState(int start, int selection) {
public static void applySuggestionWindowState(int start, int selection) {
if (BetterChatRenderer.suggestWindow == null) return;
SuggestionWindowAccessor acc = (SuggestionWindowAccessor) BetterChatRenderer.suggestWindow;
acc.setLoVisual$inWindowIndex(Math.max(0, start));
@ -110,7 +115,7 @@ final class BetterChatSuggestionState {
}
}
static void applySuggestion(int idx) {
public static void applySuggestion(int idx) {
Minecraft mc = Minecraft.getInstance();
if (!(ClientScreen.current() instanceof ChatScreen chatScreen)) return;
EditBox field = ((ChatScreenAccessor) chatScreen).getChatField();
@ -134,7 +139,7 @@ final class BetterChatSuggestionState {
}
}
static void scrollSuggestToPosition(double my) {
public static void scrollSuggestToPosition(double my) {
if (!BetterChatRenderer.suggestHasScrollbar || BetterChatRenderer.suggestVisible <= 0 || BetterChatRenderer.suggestTotal <= BetterChatRenderer.suggestVisible)
return;
float trackSpan = BetterChatRenderer.suggestTrackH - BetterChatRenderer.suggestThumbH;
@ -160,7 +165,7 @@ final class BetterChatSuggestionState {
return new CommandSuggestorBridge.SuggestionSnapshot(0, 0, 0, 0, texts, suggestions, selection, start, visible);
}
static void updateSearchHotkey(Minecraft mc, boolean chatOpen) {
public static void updateSearchHotkey(Minecraft mc, boolean chatOpen) {
if (!chatOpen || mc == null || mc.getWindow() == null) {
BetterChatRenderer.searchHotkeyDown = false;
return;
@ -177,7 +182,7 @@ final class BetterChatSuggestionState {
BetterChatRenderer.searchHotkeyDown = down;
}
static void toggleSearchMode() {
public static void toggleSearchMode() {
if (BetterChatSearch.isActive() || BetterChatSearch.hasQuery()) {
BetterChatSearch.deactivate();
} else {

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
package dev.loki.lovisual.features.gui.chat.suggestions;
import com.mojang.brigadier.context.StringRange;
import com.mojang.brigadier.suggestion.Suggestion;
@ -16,15 +16,16 @@ import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.gui.screens.ChatScreen;
import net.minecraft.client.multiplayer.PlayerInfo;
import net.minecraft.client.renderer.Rect2i;
import dev.loki.lovisual.mixins.accessors.ChatInputSuggestorAccessor;
import dev.loki.lovisual.mixins.accessors.ChatScreenAccessor;
import dev.loki.lovisual.mixins.accessors.ChatScreenSuggestorAccessor;
import dev.loki.lovisual.mixins.accessors.SuggestionWindowAccessor;
import dev.loki.lovisual.mixins.accessors.chat.ChatInputSuggestorAccessor;
import dev.loki.lovisual.mixins.accessors.chat.ChatScreenAccessor;
import dev.loki.lovisual.mixins.accessors.chat.ChatScreenSuggestorAccessor;
import dev.loki.lovisual.mixins.accessors.chat.SuggestionWindowAccessor;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat;
/**
* Reflection helpers for vanilla ChatInputSuggestor; lets us draw suggestions in custom style.

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
package dev.loki.lovisual.features.gui.chat.text;
import dev.loki.lovisual.features.command.CommandOutput;
import dev.loki.lovisual.render.engine.svg.SvgRenderOptions;
@ -15,15 +15,20 @@ import dev.loki.lovisual.util.time.GameClock;
import net.minecraft.network.chat.Component;
import java.util.List;
import dev.loki.lovisual.features.gui.chat.messages.BetterChatMessageRenderer;
import dev.loki.lovisual.features.gui.chat.model.BetterChatModel;
import dev.loki.lovisual.features.gui.chat.render.BetterChatRenderer;
import dev.loki.lovisual.features.gui.chat.text.util.BetterChatDrawUtil;
import dev.loki.lovisual.features.gui.chat.text.util.BetterChatFontUtil;
/**
* Glyph + command-prefix gradient rendering for laid-out frame lines.
*/
final class BetterChatGlyphRenderer {
public final class BetterChatGlyphRenderer {
private BetterChatGlyphRenderer() {
}
static void renderGlyphs(List<BetterChatModel.FrameLine> lines, float fontSize, float lineHeight, boolean activeChatSurface) {
public static void renderGlyphs(List<BetterChatModel.FrameLine> lines, float fontSize, float lineHeight, boolean activeChatSurface) {
if (lines == null || lines.isEmpty()) return;
float scale = BetterChatFontUtil.scaleForSize(fontSize);
TextRenderer current = null;

View file

@ -4,7 +4,7 @@
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
package dev.loki.lovisual.features.gui.chat.text;
import dev.loki.lovisual.features.command.CommandOutput;
import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry;
import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat;
@ -17,10 +17,22 @@ import net.minecraft.network.chat.Component;
import net.minecraft.util.Mth;
import java.util.Collections;
import java.util.List;
final class BetterChatLayoutEngine {
import dev.loki.lovisual.features.gui.chat.menu.BetterChatContextMenuModel;
import dev.loki.lovisual.features.gui.chat.messages.BetterChatMessageGrouping;
import dev.loki.lovisual.features.gui.chat.messages.BetterChatSearch;
import dev.loki.lovisual.features.gui.chat.model.BetterChatModel;
import dev.loki.lovisual.features.gui.chat.model.BetterChatStore;
import dev.loki.lovisual.features.gui.chat.model.BetterChatStoreManager;
import dev.loki.lovisual.features.gui.chat.model.ChatLine;
import dev.loki.lovisual.features.gui.chat.render.BetterChatRenderer;
import dev.loki.lovisual.features.gui.chat.suggestions.BetterChatCommandMerge;
import dev.loki.lovisual.features.gui.chat.suggestions.BetterChatSuggestionState;
import dev.loki.lovisual.features.gui.chat.text.BetterChatLineLayout;
import dev.loki.lovisual.features.gui.chat.text.util.BetterChatFontUtil;
public final class BetterChatLayoutEngine {
private BetterChatLayoutEngine() {
}
static void markLayoutDirty() {
public static void markLayoutDirty() {
}
static BetterChatModel.LayoutResult layoutLinesTailCached(List<ChatLine> messages, int[] messageGroups, float fontSize, float maxWidth, int maxLines) {
if (messages.isEmpty()) return new BetterChatModel.LayoutResult(Collections.emptyList(), 0);
@ -57,7 +69,7 @@ final class BetterChatLayoutEngine {
/**
* @return false when there is nothing to render this frame (caller must bail before draw phase).
*/
static boolean prepareFrame(GuiGraphicsExtractor ctx, int screenW, float baseX, float baseY) {
public static boolean prepareFrame(GuiGraphicsExtractor ctx, int screenW, float baseX, float baseY) {
BetterChatRenderer.ctxGui = ctx;
BetterChatRenderer.ctxBaseX = baseX;
BetterChatRenderer.ctxBaseY = baseY;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
package dev.loki.lovisual.features.gui.chat.text;
import net.minecraft.network.chat.HoverEvent;
import net.minecraft.network.chat.Style;
@ -17,12 +17,17 @@ import dev.loki.lovisual.render.engine.text.TextGlyphFallback;
import dev.loki.lovisual.render.engine.text.TextRenderer;
import static dev.loki.lovisual.features.theme.Theme.theme;
import dev.loki.lovisual.features.gui.chat.model.BetterChatModel;
import dev.loki.lovisual.features.gui.chat.model.ChatLine;
import dev.loki.lovisual.features.gui.chat.render.BetterChatRenderer;
import dev.loki.lovisual.features.gui.chat.text.BetterChatSegmentUtil;
import dev.loki.lovisual.features.gui.chat.text.util.BetterChatFontUtil;
/**
* Builds per-message wrapped glyph lines (cached on identity maps held by the facade).
* Extracted from {@link BetterChatRenderer}; behavior unchanged.
*/
final class BetterChatLineLayout {
public final class BetterChatLineLayout {
private BetterChatLineLayout() {
}

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
package dev.loki.lovisual.features.gui.chat.text;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.HoverEvent;
@ -20,11 +20,14 @@ import java.util.List;
import java.util.Optional;
import static dev.loki.lovisual.features.theme.Theme.theme;
import dev.loki.lovisual.features.gui.chat.model.BetterChatModel;
import dev.loki.lovisual.features.gui.chat.model.BetterChatStoreManager;
import dev.loki.lovisual.features.gui.chat.text.util.BetterChatHoverCache;
/**
* Segment flattening and per-segment color resolution for the chat line layout.
*/
final class BetterChatSegmentUtil {
public final class BetterChatSegmentUtil {
private BetterChatSegmentUtil() {
}

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
package dev.loki.lovisual.features.gui.chat.text.util;
import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette;
import dev.loki.lovisual.features.gui.hud.HudRenderUtil;
@ -15,31 +15,32 @@ import dev.loki.lovisual.render.helpers.SystemCursor;
import net.minecraft.util.Mth;
import static dev.loki.lovisual.features.theme.Theme.theme;
import dev.loki.lovisual.features.gui.chat.render.BetterChatRenderer;
/**
* Low-level draw primitives + color helpers shared by the chat renderer helpers.
*/
final class BetterChatDrawUtil {
public final class BetterChatDrawUtil {
private BetterChatDrawUtil() {
}
static int withAlpha(int argb, int alpha) {
public static int withAlpha(int argb, int alpha) {
return (argb & 0x00FFFFFF) | ((alpha & 0xFF) << 24);
}
static int mulAlpha(int argb, float a) {
public static int mulAlpha(int argb, float a) {
int alpha = (int) (((argb >>> 24) & 0xFF) * a);
return (argb & 0x00FFFFFF) | (Mth.clamp(alpha, 0, 255) << 24);
}
static void applyColor(int argb) {
public static void applyColor(int argb) {
BetterChatRenderer.TMP_COLOR.a = (argb >>> 24) & 0xFF;
BetterChatRenderer.TMP_COLOR.r = (argb >>> 16) & 0xFF;
BetterChatRenderer.TMP_COLOR.g = (argb >>> 8) & 0xFF;
BetterChatRenderer.TMP_COLOR.b = argb & 0xFF;
}
static void drawRoundedRect(float x, float y, float w, float h, float radius, int argb) {
public static void drawRoundedRect(float x, float y, float w, float h, float radius, int argb) {
if (BetterChatRenderer.renderer == null) return;
if (((argb >>> 24) & 0xFF) <= 0) return;
BetterChat cfg = BetterChat.get();
@ -47,7 +48,7 @@ final class BetterChatDrawUtil {
HudRenderUtil.drawHudBackground(BetterChatRenderer.renderer, x, y, w, h, radius, 1.1f, argb, useGradient);
}
static void drawRoundedRectStrokeGradient(float x, float y, float w, float h, float radius, float thickness,
public static void drawRoundedRectStrokeGradient(float x, float y, float w, float h, float radius, float thickness,
int startArgb, int endArgb) {
if (BetterChatRenderer.renderer == null) return;
if (((startArgb >>> 24) & 0xFF) <= 0 && ((endArgb >>> 24) & 0xFF) <= 0) return;
@ -69,7 +70,7 @@ final class BetterChatDrawUtil {
}
}
static void drawGlassPill(float x, float y, float w, float h, float radius, float alpha, boolean softPanel) {
public static void drawGlassPill(float x, float y, float w, float h, float radius, float alpha, boolean softPanel) {
if (BetterChatRenderer.renderer == null || w <= 0f || h <= 0f) return;
BetterChat settings = BetterChat.get();
float drawAlpha = Mth.clamp(alpha, 0.0f, 1.0f);
@ -107,7 +108,7 @@ final class BetterChatDrawUtil {
}
}
static void drawLiquidHoverPill(float x, float y, float w, float h, float radius, float alpha) {
public static void drawLiquidHoverPill(float x, float y, float w, float h, float radius, float alpha) {
if (BetterChatRenderer.renderer == null || w <= 0f || h <= 0f || alpha <= 0.001f) return;
flushRenderer();
BetterChatRenderer.renderer.liquidGlassRect(
@ -130,7 +131,7 @@ final class BetterChatDrawUtil {
withAlpha(theme().accent(), 0x34));
}
static void drawScrollbarGlass(float trackX, float trackY, float trackW, float trackH,
public static void drawScrollbarGlass(float trackX, float trackY, float trackW, float trackH,
float thumbY, float thumbH, boolean hot, int trackAlpha, int thumbAlpha) {
if (BetterChatRenderer.renderer == null || trackW <= 0f || trackH <= 0f || thumbH <= 0f) return;
SettingsGuiPalette palette = SettingsGuiPalette.current();
@ -143,7 +144,7 @@ final class BetterChatDrawUtil {
BetterChatRenderer.renderer.roundedRectGradientQuad(trackX, thumbY, trackW, thumbH, radius, 1.0f, handleA, handleB, handleB, handleA);
}
static void renderScrollbar(float boxX, float boxY, float boxW, float boxH,
public static void renderScrollbar(float boxX, float boxY, float boxW, float boxH,
int startLine, int visibleLines, int totalLines, double mouseX, double mouseY) {
if (visibleLines <= 0 || totalLines <= visibleLines) {
BetterChatRenderer.sbEnabled = false;
@ -177,7 +178,7 @@ final class BetterChatDrawUtil {
drawScrollbarGlass(trackX, trackY, trackW, trackH, thumbY, thumbH, hot, trackAlpha, thumbAlpha);
}
static boolean isScrollbarHovered(double mx, double my) {
public static boolean isScrollbarHovered(double mx, double my) {
float hitPad = 5f;
return BetterChatRenderer.sbEnabled &&
mx >= BetterChatRenderer.sbTrackX - hitPad &&
@ -186,7 +187,7 @@ final class BetterChatDrawUtil {
my <= BetterChatRenderer.sbTrackY + BetterChatRenderer.sbTrackH + hitPad;
}
static void scrollToPosition(double my) {
public static void scrollToPosition(double my) {
if (!BetterChatRenderer.sbEnabled || BetterChatRenderer.sbVisibleLines <= 0 || BetterChatRenderer.sbTotalLines <= BetterChatRenderer.sbVisibleLines)
return;
float trackSpan = BetterChatRenderer.sbTrackH - BetterChatRenderer.sbThumbH;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
package dev.loki.lovisual.features.gui.chat.text.util;
import dev.loki.lovisual.render.engine.text.FontInfo;
import dev.loki.lovisual.render.engine.text.Fonts;
@ -14,20 +14,22 @@ import dev.loki.lovisual.render.engine.text.TextRenderer;
import dev.loki.lovisual.render.engine.text.VanillaTextRenderer;
import net.minecraft.network.chat.Style;
import net.minecraft.util.Mth;
import dev.loki.lovisual.features.gui.chat.render.BetterChatRenderer;
import dev.loki.lovisual.features.gui.chat.text.util.BetterChatDrawUtil;
/**
* Font resolution, glyph sizing and plain-text draw helpers for the chat renderer.
* All public-facing metrics share the renderer singleton held on {@link BetterChatRenderer}.
*/
final class BetterChatFontUtil {
public final class BetterChatFontUtil {
private BetterChatFontUtil() {
}
static TextRenderer getInterRegular() {
public static TextRenderer getInterRegular() {
return Fonts.renderer("Inter", FontInfo.Type.Regular, TextRenderer.get());
}
static TextRenderer getIosevkaRegular() {
public static TextRenderer getIosevkaRegular() {
return Fonts.renderer("Iosevka", FontInfo.Type.Regular, TextRenderer.get());
}
@ -43,7 +45,7 @@ final class BetterChatFontUtil {
return Fonts.renderer("Iosevka", FontInfo.Type.BoldItalic, getIosevkaBold());
}
static TextRenderer fontRenderer(String key) {
public static TextRenderer fontRenderer(String key) {
if (key == null) return getIosevkaRegular();
return switch (key) {
case "iosevka_bold" -> getIosevkaBold();
@ -59,12 +61,12 @@ final class BetterChatFontUtil {
return TextGlyphFallback.rendererForGlyph(preferred, codePoint);
}
static String fontForGlyph(String baseFont, int codePoint) {
public static String fontForGlyph(String baseFont, int codePoint) {
TextRenderer preferred = fontRenderer(baseFont);
return TextGlyphFallback.fontKeyForGlyph(baseFont, preferred, codePoint, "iosevka_medium");
}
static String fontForStyle(Style style) {
public static String fontForStyle(Style style) {
boolean bold = style.isBold();
boolean italic = style.isItalic();
if (bold && italic) return "iosevka_bold_italic";
@ -73,15 +75,15 @@ final class BetterChatFontUtil {
return "iosevka_medium";
}
static float scaleForSize(float size) {
public static float scaleForSize(float size) {
return size / 18.0f;
}
static float svgGlyphSize(TextRenderer tr, float size, boolean shadow) {
public static float svgGlyphSize(TextRenderer tr, float size, boolean shadow) {
return Math.max(1.0f, textHeight(tr, size)) * 0.92f;
}
static float textHeight(TextRenderer tr, float size) {
public static float textHeight(TextRenderer tr, float size) {
if (tr == null) return 0f;
float scale = scaleForSize(size);
tr.begin(scale, true, false);
@ -92,7 +94,7 @@ final class BetterChatFontUtil {
}
}
static float textWidth(TextRenderer tr, String text, float size) {
public static float textWidth(TextRenderer tr, String text, float size) {
if (tr == null || text == null || text.isEmpty()) return 0f;
float scale = scaleForSize(size);
float svgAdvance = svgGlyphSize(tr, size, false);
@ -126,7 +128,7 @@ final class BetterChatFontUtil {
}
}
static void drawText(TextRenderer tr, String text, float x, float y, float size, int argb, boolean shadow) {
public static void drawText(TextRenderer tr, String text, float x, float y, float size, int argb, boolean shadow) {
if (tr == null || text == null || text.isEmpty()) return;
if (((argb >>> 24) & 0xFF) <= 0) return;
float scale = scaleForSize(size);
@ -167,7 +169,7 @@ final class BetterChatFontUtil {
}
}
static String fitText(TextRenderer font, String text, float size, float maxWidth) {
public static String fitText(TextRenderer font, String text, float size, float maxWidth) {
if (text == null) return "";
if (textWidth(font, text, size) <= maxWidth) return text;
String suffix = "...";
@ -179,7 +181,7 @@ final class BetterChatFontUtil {
return end <= 0 ? suffix : text.substring(0, end) + suffix;
}
static String safeSub(String s, int start, int end) {
public static String safeSub(String s, int start, int end) {
if (s == null) return "";
int len = s.length();
start = Math.max(0, Math.min(len, start));

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
package dev.loki.lovisual.features.gui.chat.text.util;
import com.google.gson.*;
import com.mojang.serialization.JsonOps;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
package dev.loki.lovisual.features.gui.chat.text.util;
import dev.loki.lovisual.util.item.TopEnchantUtil;
import net.minecraft.client.Minecraft;
@ -25,6 +25,9 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat;
import dev.loki.lovisual.features.gui.chat.model.BetterChatStoreManager;
import dev.loki.lovisual.features.gui.chat.text.util.BetterChatHoverCache;
/**
* Hover payload helpers + cache fallbacks.

View file

@ -3,7 +3,7 @@ package dev.loki.lovisual.features.gui.clickgui;
import dev.loki.lovisual.render.engine.animation.AnimationUtility;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
final class ClickGuiDrawHelpers {
public final class ClickGuiDrawHelpers {
private ClickGuiDrawHelpers() {
}

View file

@ -3,12 +3,12 @@ package dev.loki.lovisual.features.gui.clickgui;
import dev.loki.lovisual.features.gui.clickgui.editor.ClickGuiTextEditorState;
import dev.loki.lovisual.features.gui.clickgui.picker.ClickGuiPickerState;
import dev.loki.lovisual.features.gui.clickgui.protocol.CombatProtocolHeuristicsEditorState;
import dev.loki.lovisual.features.gui.clickgui.settings.TextEditorOwner;
import dev.loki.lovisual.features.gui.clickgui.settings.TextSetting;
import dev.loki.lovisual.features.gui.clickgui.settings.owner.TextEditorOwner;
import dev.loki.lovisual.features.gui.clickgui.settings.text.TextSetting;
import dev.loki.lovisual.features.module.ModuleManager;
import dev.loki.lovisual.util.text.ClipboardUtil;
final class ClickGuiEditorManager {
public final class ClickGuiEditorManager {
private ClickGuiEditorManager() {
}

View file

@ -5,7 +5,7 @@ import dev.loki.lovisual.render.engine.text.FontInfo;
import dev.loki.lovisual.render.engine.text.Fonts;
import dev.loki.lovisual.render.engine.text.TextRenderer;
final class ClickGuiFonts {
public final class ClickGuiFonts {
private ClickGuiFonts() {
}

Some files were not shown because too many files have changed in this diff Show more