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

85
TODO.md
View file

@ -29,6 +29,10 @@
- [x] Фаза 5: Нормализация модулей (ленивый Minecraft, GameClock) - [x] Фаза 5: Нормализация модулей (ленивый Minecraft, GameClock)
- [x] Фаза 6: Тесты (EventBus, конфиг, утилиты) — см. `combatant-client-26.2/TODO.md` - [x] Фаза 6: Тесты (EventBus, конфиг, утилиты) — см. `combatant-client-26.2/TODO.md`
- [ ] Фаза 7: Новые модули по мотивам SoupVisuals — см. ниже - [ ] Фаза 7: Новые модули по мотивам SoupVisuals — см. ниже
- [ ] Фаза 8.5: Аудит структуры ≤200/≤4 (244 файла, 86 папок) — гейт перед Фазой 9.2,
план в `combatant-client-26.2/TODO.md`
- [ ] Фаза 9: Оптимизация FPS (9.1 фиксы мёртвых грузов → 9.2 модуль Optimize),
план в `combatant-client-26.2/TODO.md`
--- ---
@ -58,6 +62,87 @@
иконки-спутники (Atom/Ball/Cat/Donut/Flame/Flower/Gem/Ghost/MCTiers), трейл иконки-спутники (Atom/Ball/Cat/Donut/Flame/Flower/Gem/Ghost/MCTiers), трейл
(цвет sync/custom, длина, толщина), звуки (типы + громкость), частицы (цвет sync/custom, длина, толщина), звуки (типы + громкость), частицы
(у Soup 315 стр.). Референс: `other/Trinket.java` (у Soup 315 стр.). Референс: `other/Trinket.java`
- [ ] **Cosmetics Overhaul (Unified Cosmetics)** — сейчас модуль бесполезен (next/prev
keybind без экрана). План (обсуждён в брейнсторме, черновик, детали могут
измениться):
- **Слоты вместо типов**: новый `CosmeticSlot` enum — `MODEL, PET, HEAD,
SHOULDERS, BACK, WAIST, LEGS, HELD` (8 шт). `CosmeticType` (MODEL/PET/ACCESSORY)
остаётся внутри `CosmeticModel` как есть (анимация/рендер-логика не трогается).
- Аксессуары (ACCESSORY) получают поле `slot` — парсится из `avatar.json`
(`"slot": "head"`), при отсутствии — вывод по имени папки (существующая
эвристика witch/hat→HEAD, wing→BACK, wheel→WAIST, иначе→HELD).
- `CosmeticManager`: `EnumMap<CosmeticSlot, CosmeticModel>` вместо 3 полей.
MODEL/PET — по одному активному. 6 accessory-слотов — **можно носить
несколько одновременно** (шляпа + крылья + пояс разом).
- Экран `CosmeticsScreen`: левая панель — живая 3D-модель игрока
(`InventoryScreen.renderEntityInInventoryFollowsMouse` или свой вызов
EntityRenderDispatcher), drag ЛКМ по превью — вращение (yaw/pitch,
pitch ограничен ±30°), скролл — zoom (0.5–2.0), кнопка сброса угла.
Открывается через **ПКМ по модулю в ClickGUI** (спецкейс вместо обычной
settings-панели — прецедент есть в `ClickGuiEditorScreen`).
- Hotspot-зоны поверх 3D-модели (HEAD/SHOULDERS/BACK/WAIST/LEGS + иконка
HELD сбоку) — клик открывает правую панель выбора модели для этого слота.
- Offset/rotation/scale — **на слот** (не на модель, т.к. модулей может быть
десятки, а статичных SettingDef-полей на каждую не сделать): 6 стандартных
num()-хелперов модуля (per-slot), visibleWhen(активный слот). Смена модели
в слоте может потребовать подстройки — ок, вписывается в существующий
SettingDef/SettingFactory конвейер без новой инфраструктуры.
- Цвета из Theme.theme() (windowBg/accent/textMuted), файлы по правилам
≤200 строк/≤4 файла на папку: `CosmeticsModule`, `CosmeticsScreen`,
`CosmeticsSlot`(=CosmeticSlot), `CosmeticsSlotPanel`.
- i18n: `module.cosmetics.*`, `setting.cosmetics.active_slot`,
`offset_x/y/z`, `rotation`, `scale`, `setting.cosmetics.slot.<slot>` и т.д.
(en_us + ru_ru обязательно).
- Открытые вопросы для следующего захода: точные хитбокс-координаты зон
по проценту региона превью, формат хранения per-slot offset в .lvcfg,
нужен ли предпросмотр модели до эквипа в правой панели.
- **Портирование процедурных head/back-модулей в слоты** — из всех
`visuals/*` только 3 модуля реально являются «надетыми на игрока»
процедурными косметиками (нашёл через grep по HeadAnchor-хелперу):
- `ChinaHat` (+`ChinaHatModel`, `ChinaHatPalette`, 295+107+144 стр.) —
конус на голове → встроенная опция слота **HEAD**.
- `TazikHat` (859 стр.! требует разбивки на файлы ≤200 стр. при
портировании — геометрия/палитра/модуль отдельно, как у ChinaHat) —
«тазик»/чаша на голове → **HEAD**.
- `BackSword` (287 стр.) — катана на спине → **BACK**.
Остальные visuals-модули (ESP, Chams, Trails, Rain, PostFX и т.д.) —
НЕ переносятся, это общие эффекты, а не надеваемые предметы.
План: каждый становится встроенным вариантом (built-in entry, не
.bbmodel) внутри соответствующего слота — юзер выбирает между
своими .bbmodel-моделями И этими процедурными built-in вариантами
в одном и том же слоте. Настройки самого варианта (цвет/размер/сегменты
и т.д.) показываются в правой панели слота, когда built-in выбран
активным. Старые `@ModuleInfo`-модули `chinahat`/`tazikhat`/`backsword`
удаляются как отдельные модули из списка Visuals (их функциональность
полностью переезжает в Cosmetics) — конфиг-миграция/бэккомпат не нужны
(легит-клиент, публичного релиза со старыми конфигами ещё не было —
уточнить у пользователя перед удалением).
- **"None" в каждом слоте** — обязательный первый вариант в списке выбора
любого слота (Model/Pet/Head/Shoulders/Back/Waist/Legs/Held): "ничего
не надето", слот пуст, `CosmeticManager.active` для этого слота = null.
Не путать с выключенным модулем `CosmeticDisplay` целиком — None это
per-slot состояние, остальные слоты продолжают рендериться.
- Подтверждено пользователем: точно только 3 built-in кандидата
(ChinaHat/TazikHat/BackSword), остальные visuals-модули не переносятся.
- **Решённые вопросы (финал черновика):**
1. Hotspot-координаты — **вариант B**: проекция костей игрока (голова/тело/
ноги vanilla-модели) в экранные координаты через `ScreenProjection`
(уже есть в `render/helpers/`), а не фиксированные проценты региона —
точнее держит зоны при вращении/приседе. Fallback на фикс-проценты,
если в реализации окажется дороже, чем стоит.
2. Хранение offset/rot/scale — **обычные `NumberValue`-поля модуля**
(6 слотов × offset_x/y/z/rot/scale), сохраняются в `.lvcfg` как
обычные настройки — без нового ConfigValue-типа.
3. Правая панель — **live-apply**: клик по модели в списке слота сразу
применяет и показывает на превью, без отдельной кнопки Apply.
4. Built-in процедурные варианты (ChinaHat/TazikHat/BackSword) — **просто
обычные записи в списке слота** (с иконкой-превью), не выделяются в
отдельную секцию; настройки открываются так же, как у .bbmodel-моделей.
5. `TazikHat` (859 стр.) при портировании — **разбить как ChinaHat**:
`TazikHatGeometry` + `TazikHatPalette` + встраиваемый рендер-класс без
собственного `@ModuleInfo`/Module-обвязки (вызывается из слота HEAD).
6. Старые модули `chinahat`/`tazikhat`/`backsword` — **удаляются
полностью** из списка Visuals, функциональность только в Cosmetics.
- [ ] **Balancer** (mctiers) — тиры игроков с mctiers.info (у Soup отдельная категория - [ ] **Balancer** (mctiers) — тиры игроков с mctiers.info (у Soup отдельная категория
MCTiers). Референс: `mctiers/Balancer.java` MCTiers). Референс: `mctiers/Balancer.java`
- [ ] **HandsShader: новые шейдеры руки** — у Soup 10 режимов (Solid, Chroma, Balatro, - [ ] **HandsShader: новые шейдеры руки** — у Soup 10 режимов (Solid, Chroma, Balatro,

View file

@ -241,3 +241,161 @@
- [ ] Не смешивать с Фазой 7: разбивка структуры — отдельными проходами, иначе diff-каша - [ ] Не смешивать с Фазой 7: разбивка структуры — отдельными проходами, иначе diff-каша
### Проверка: `./gradlew test` зелёный ### Проверка: `./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; 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.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; 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.account.AccountConfig;
import dev.loki.lovisual.features.command.CommandManager; import dev.loki.lovisual.features.command.CommandManager;
import dev.loki.lovisual.features.gui.clickgui.ClickGuiRenderer; 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.HudElements;
import dev.loki.lovisual.features.gui.hud.HudRenderSpace; import dev.loki.lovisual.features.gui.hud.HudRenderSpace;
import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; 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.pvp.PvpTracker;
import dev.loki.lovisual.util.target.TargetManager; import dev.loki.lovisual.util.target.TargetManager;
import dev.loki.lovisual.util.time.TimerController; import dev.loki.lovisual.util.time.TimerController;
import dev.loki.lovisual.features.gui.clickgui.settings.bind.KeyBindSetting;
public class LoVisual implements ClientModInitializer { public class LoVisual implements ClientModInitializer {

View file

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

View file

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

View file

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

View file

@ -30,7 +30,7 @@ import net.fabricmc.loader.api.ModContainer;
import java.nio.file.Path; import java.nio.file.Path;
final class LoVisualAddonContextImpl implements LoVisualAddonContext { public final class LoVisualAddonContextImpl implements LoVisualAddonContext {
private final AddonRegistration registration; private final AddonRegistration registration;
LoVisualAddonContextImpl(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.addon.LoVisualAddonRuntimeContext;
import dev.loki.lovisual.api.v0.client.LoVisualClientApi; import dev.loki.lovisual.api.v0.client.LoVisualClientApi;
final class LoVisualAddonRuntimeContextImpl implements LoVisualAddonRuntimeContext { public final class LoVisualAddonRuntimeContextImpl implements LoVisualAddonRuntimeContext {
private final String addonId; private final String addonId;
LoVisualAddonRuntimeContextImpl(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.ArrayList;
import java.util.List; import java.util.List;
final class LoVisualModuleLoadContextImpl implements LoVisualModuleLoadContext { public final class LoVisualModuleLoadContextImpl implements LoVisualModuleLoadContext {
private final AddonRegistration registration; private final AddonRegistration registration;
private final List<String> pendingExclusions = new ArrayList<>(); 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.config.values.ModeValue;
import dev.loki.lovisual.features.module.Module; import dev.loki.lovisual.features.module.Module;
final class ModuleExtensionContextImpl implements ModuleExtensionContext { public final class ModuleExtensionContextImpl implements ModuleExtensionContext {
private final String addonId; private final String addonId;
private final Module module; private final Module module;

View file

@ -7,7 +7,7 @@
package dev.loki.lovisual.api.v0.addon; 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.features.command.ClientCommand;
import dev.loki.lovisual.api.v0.clickgui.LoVisualClickGuiSection; import dev.loki.lovisual.api.v0.clickgui.LoVisualClickGuiSection;
import dev.loki.lovisual.features.gui.hud.AbstractHudElement; import dev.loki.lovisual.features.gui.hud.AbstractHudElement;

View file

@ -7,7 +7,7 @@
package dev.loki.lovisual.api.v0.addon; 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; 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.ArrayList;
import java.util.List; import java.util.List;
final class XaeroWaypointRuntime { public final class XaeroWaypointRuntime {
private XaeroWaypointRuntime() { private XaeroWaypointRuntime() {
} }

View file

@ -7,7 +7,6 @@
package dev.loki.lovisual.config; package dev.loki.lovisual.config;
import dev.loki.lovisual.config.values.*;
import dev.loki.lovisual.config.values.*; import dev.loki.lovisual.config.values.*;
import dev.loki.lovisual.render.iris.IrisRuntime; import dev.loki.lovisual.render.iris.IrisRuntime;
import dev.loki.lovisual.util.logging.DebugLog; 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.common.CommonSettingSchema;
import dev.loki.lovisual.config.values.*; 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.lang.reflect.Field;
import java.util.ArrayList; import java.util.ArrayList;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,13 +5,13 @@
* Licensed under the GNU General Public License v3.0. * 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.SettingDef;
import dev.loki.lovisual.config.SettingOwner; import dev.loki.lovisual.config.SettingOwner;
import dev.loki.lovisual.events.Event; import dev.loki.lovisual.events.Event;
import dev.loki.lovisual.features.gui.clickgui.settings.Setting; import dev.loki.lovisual.features.gui.clickgui.settings.core.Setting;
import dev.loki.lovisual.features.gui.clickgui.settings.SettingFactory; import dev.loki.lovisual.features.gui.clickgui.settings.core.SettingFactory;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@
package dev.loki.lovisual.features.command; 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.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;

View file

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

View file

@ -15,6 +15,7 @@ import dev.loki.lovisual.features.module.Module;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import dev.loki.lovisual.features.gui.hud.draggable.impl.panels.ModuleList;
@CommandInfo( @CommandInfo(
id = "hide", 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.CommandInfo;
import dev.loki.lovisual.features.command.CommandOutput; import dev.loki.lovisual.features.command.CommandOutput;
import dev.loki.lovisual.util.player.NetworkStatsUtil; import dev.loki.lovisual.util.player.NetworkStatsUtil;
import dev.loki.lovisual.features.gui.hud.draggable.impl.stats.Ping;
@CommandInfo( @CommandInfo(
id = "ping", id = "ping",

View file

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

View file

@ -2,54 +2,89 @@ package dev.loki.lovisual.features.cosmetic;
import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Axis; 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 { public final class CosmeticAttachmentTransforms {
private CosmeticAttachmentTransforms() { private CosmeticAttachmentTransforms() {
} }
public static void apply(PoseStack matrices, CosmeticType type, CosmeticModel model) { /**
if (type == CosmeticType.PET) { * 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);
}
}
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. // TODO: replace with CosmeticAnimator.getPetHoverOffset() once the animator is ported.
float bob = 0f; float bob = 0f;
matrices.mulPose(Axis.YP.rotationDegrees(180f)); matrices.mulPose(Axis.YP.rotationDegrees(180f));
matrices.translate(0.45f, 2.15f + bob, 0.12f); matrices.translate(0.45f, 2.15f + bob, 0.12f);
matrices.scale(0.55f, 0.55f, 0.55f); matrices.scale(0.55f, 0.55f, 0.55f);
return;
} }
if (type == CosmeticType.ACCESSORY && model != null) { case HEAD -> {
String folder = model.getFolderName().toLowerCase();
if (folder.contains("witch") || folder.contains("hat")) {
matrices.translate(0f, 1.72f, 0f); matrices.translate(0f, 1.72f, 0f);
matrices.scale(0.52f, 0.52f, 0.52f); 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.translate(0f, 0.1f, -0.3f);
matrices.mulPose(Axis.YP.rotationDegrees(180f)); matrices.mulPose(Axis.YP.rotationDegrees(180f));
matrices.scale(0.85f, 0.85f, 0.85f); matrices.scale(0.85f, 0.85f, 0.85f);
} else if (folder.contains("wheel")) { }
case WAIST -> {
matrices.translate(0f, 0.55f, 0.25f); matrices.translate(0f, 0.55f, 0.25f);
matrices.scale(0.55f, 0.55f, 0.55f); 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.translate(0f, 1.3f, 0f);
matrices.scale(0.5f, 0.5f, 0.5f); 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) { public static void applyPreview(PoseStack matrices, CosmeticModel model) {
if (model == null) return; if (model == null) return;
String folder = model.getFolderName().toLowerCase(); CosmeticSlot slot = model.getSlot();
if (model.getType() == CosmeticType.PET) { if (slot == CosmeticSlot.PET) {
matrices.translate(6f, 22f, 1f); matrices.translate(6f, 22f, 1f);
matrices.scale(1.8f, 1.8f, 1.8f); matrices.scale(1.8f, 1.8f, 1.8f);
} else if (model.getType() == CosmeticType.ACCESSORY) { } else if (slot == CosmeticSlot.HEAD) {
if (folder.contains("witch") || folder.contains("hat")) {
matrices.translate(0f, 20f, 0f); matrices.translate(0f, 20f, 0f);
} else if (folder.contains("wing")) { } else if (slot == CosmeticSlot.BACK) {
matrices.translate(0f, 2f, -4f); matrices.translate(0f, 2f, -4f);
matrices.scale(1.3f, 1.3f, 1.3f); matrices.scale(1.3f, 1.3f, 1.3f);
} else if (folder.contains("wheel")) { } else if (slot == CosmeticSlot.WAIST) {
matrices.translate(0f, 6f, 3f); matrices.translate(0f, 6f, 3f);
} }
} }
} }
}

View file

@ -1,15 +1,18 @@
package dev.loki.lovisual.features.cosmetic; 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). * Rendering and animation live in separate classes (not part of this core pass).
*/ */
public final class CosmeticManager { public final class CosmeticManager {
private static final CosmeticManager INSTANCE = new CosmeticManager(); private static final CosmeticManager INSTANCE = new CosmeticManager();
private CosmeticModel activeModel; private final Map<CosmeticSlot, CosmeticModel> active = new EnumMap<>(CosmeticSlot.class);
private CosmeticModel activePet;
private CosmeticModel activeAccessory;
private CosmeticManager() { private CosmeticManager() {
} }
@ -23,52 +26,55 @@ public final class CosmeticManager {
* (by id) against the freshly loaded models. * (by id) against the freshly loaded models.
*/ */
public void init() { public void init() {
String modelId = activeModel != null ? activeModel.getId() : null; Map<CosmeticSlot, String> equippedIds = new EnumMap<>(CosmeticSlot.class);
String petId = activePet != null ? activePet.getId() : null; for (Map.Entry<CosmeticSlot, CosmeticModel> entry : active.entrySet()) {
String accessoryId = activeAccessory != null ? activeAccessory.getId() : null; if (entry.getValue() != null) {
equippedIds.put(entry.getKey(), entry.getValue().getId());
}
}
for (CosmeticModel model : CosmeticRepository.get().all()) { for (CosmeticModel model : CosmeticRepository.get().all()) {
model.resetTexture(); model.resetTexture();
} }
CosmeticRepository.get().reload(); CosmeticRepository.get().reload();
active.clear();
CosmeticRepository repo = CosmeticRepository.get(); CosmeticRepository repo = CosmeticRepository.get();
activeModel = modelId != null ? repo.get(modelId) : null; for (Map.Entry<CosmeticSlot, String> entry : equippedIds.entrySet()) {
activePet = petId != null ? repo.get(petId) : null; CosmeticModel resolved = repo.get(entry.getValue());
activeAccessory = accessoryId != null ? repo.get(accessoryId) : null; if (resolved != null) {
active.put(entry.getKey(), resolved);
}
}
} }
public CosmeticModel getActive(CosmeticType type) { public CosmeticModel getActive(CosmeticSlot slot) {
return switch (type) { return active.get(slot);
case PET -> activePet;
case ACCESSORY -> activeAccessory;
case MODEL -> activeModel;
};
} }
public void setActive(CosmeticType type, CosmeticModel model) { public void setActive(CosmeticSlot slot, CosmeticModel model) {
if (model != null) { if (model != null) {
CosmeticModel resolved = CosmeticRepository.get().get(model.getId()); CosmeticModel resolved = CosmeticRepository.get().get(model.getId());
if (resolved != null) { if (resolved != null) {
model = resolved; model = resolved;
} }
} }
switch (type) { if (model == null) {
case PET -> activePet = model; active.remove(slot);
case ACCESSORY -> activeAccessory = model; } else {
case MODEL -> activeModel = model; active.put(slot, model);
} }
} }
public void clear(CosmeticType type) { public void clear(CosmeticSlot slot) {
setActive(type, null); setActive(slot, null);
} }
public boolean hasAnyActive() { public boolean hasAnyActive() {
return activeModel != null || activePet != null || activeAccessory != null; return !active.isEmpty();
} }
public boolean isActive() { 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 accentColor;
private final String folderName; private final String folderName;
private final CosmeticType type; private final CosmeticType type;
private final CosmeticSlot slot;
private final File externalDirectory; private final File externalDirectory;
@Setter private String source = "Своя"; @Setter private String source = "Своя";
@ -63,18 +64,27 @@ public class CosmeticModel {
private CosmeticTextureSlot fallbackSlot; private CosmeticTextureSlot fallbackSlot;
private CosmeticModel(String id, String name, String description, String accentColor, 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.id = id;
this.name = name; this.name = name;
this.description = description; this.description = description;
this.accentColor = accentColor; this.accentColor = accentColor;
this.folderName = folderName; this.folderName = folderName;
this.type = type; this.type = type;
this.slot = slot != null ? slot : defaultSlotFor(type, folderName);
this.externalDirectory = externalDirectory; 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) { 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.rootBones.addAll(rootBones);
copy.renderRoots.addAll(renderRoots); copy.renderRoots.addAll(renderRoots);
copy.boneMap.putAll(boneMap); copy.boneMap.putAll(boneMap);
@ -141,7 +151,8 @@ public class CosmeticModel {
: avatar.has("type") ? avatar.get("type").getAsString() : null : 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); InputStream modelStream = openModelStream(externalDir, folderName);
if (modelStream == null) { if (modelStream == null) {
@ -392,6 +403,7 @@ public class CosmeticModel {
parent.getAccentColor(), parent.getAccentColor(),
petFolder, petFolder,
CosmeticType.PET, CosmeticType.PET,
null,
null null
); );
pet.source = parent.source; pet.source = parent.source;

View file

@ -30,10 +30,10 @@ public final class CosmeticRenderer {
private 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; if (renderer == null || player == null) return;
CosmeticModel model = CosmeticManager.get().getActive(type); CosmeticModel model = CosmeticManager.get().getActive(slot);
if (model == null) return; if (model == null) return;
model.loadTexture(); model.loadTexture();
@ -45,7 +45,7 @@ public final class CosmeticRenderer {
PoseStack matrices = new PoseStack(); PoseStack matrices = new PoseStack();
matrices.translate(pos.x, pos.y, pos.z); 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)); matrices.mulPose(Axis.YP.rotationDegrees(180f - bodyYaw));
Map<Integer, List<PendingFace>> byTexture = new HashMap<>(); Map<Integer, List<PendingFace>> byTexture = new HashMap<>();
@ -58,14 +58,14 @@ public final class CosmeticRenderer {
Vec3 cam = RenderState.cameraPos; Vec3 cam = RenderState.cameraPos;
for (Map.Entry<Integer, List<PendingFace>> entry : byTexture.entrySet()) { for (Map.Entry<Integer, List<PendingFace>> entry : byTexture.entrySet()) {
CosmeticTextureSlot slot = model.getTextureSlot(entry.getKey()); CosmeticTextureSlot textureSlot = model.getTextureSlot(entry.getKey());
if (slot == null) continue; if (textureSlot == null) continue;
if (!slot.isLoaded()) { if (!textureSlot.isLoaded()) {
model.ensureTextureSlotLoaded(slot); 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; if (texture == null || texture.getTextureView() == null || texture.getSampler() == null) continue;
Renderer3D.BatchBindings bindings = Renderer3D.BatchBindings.none() 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(); 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) { public CosmeticModel get(String id) {
return catalog.get(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.Collections;
import java.util.List; 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 * 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} * (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. * 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() { private BetterChatViewport() {
} }
@ -67,7 +71,7 @@ final class BetterChatViewport {
return safe; return safe;
} }
static void composeFrame() { public static void composeFrame() {
List<BetterChatModel.VisualLine> allLines = BetterChatRenderer.ctxAllLines; List<BetterChatModel.VisualLine> allLines = BetterChatRenderer.ctxAllLines;
BetterChatModel.LayoutResult layoutResult = new BetterChatModel.LayoutResult(allLines, BetterChatRenderer.ctxMessagesSeen); BetterChatModel.LayoutResult layoutResult = new BetterChatModel.LayoutResult(allLines, BetterChatRenderer.ctxMessagesSeen);
List<ChatLine> messages = BetterChatRenderer.lastMessages; List<ChatLine> messages = BetterChatRenderer.lastMessages;

View file

@ -5,25 +5,25 @@
* Licensed under the GNU General Public License v3.0. * 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.client.gui.components.EditBox;
import net.minecraft.util.Mth; import net.minecraft.util.Mth;
/** /**
* EditBox selection introspection + word-boundary helper, independent of the chat renderer. * EditBox selection introspection + word-boundary helper, independent of the chat renderer.
*/ */
final class BetterChatEditBoxUtil { public final class BetterChatEditBoxUtil {
private BetterChatEditBoxUtil() { private BetterChatEditBoxUtil() {
} }
static int selectionStart(EditBox field, int fallback) { public static int selectionStart(EditBox field, int fallback) {
if (field == null) return fallback; if (field == null) return fallback;
return Mth.clamp(field.getCursorPosition(), 0, field.getValue().length()); 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; if (field == null) return fallback;
try { try {
return Mth.clamp(((TextFieldWidgetAccessor) field).lovisual$getSelectionEnd(), 0, field.getValue().length()); 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; if (field == null) return;
field.moveCursorTo(Mth.clamp(pos, 0, field.getValue().length()), false); 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 ""; if (text == null || text.isEmpty()) return "";
int idx = Mth.clamp(charIndex, 0, Math.max(0, text.length() - 1)); int idx = Mth.clamp(charIndex, 0, Math.max(0, text.length() - 1));
int start = idx; int start = idx;

View file

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

View file

@ -4,8 +4,8 @@
* *
* Licensed under the GNU General Public License v3.0. * 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.ChatScreenAccessor; import dev.loki.lovisual.mixins.accessors.chat.ChatScreenAccessor;
import dev.loki.lovisual.render.engine.text.FontInfo; import dev.loki.lovisual.render.engine.text.FontInfo;
import dev.loki.lovisual.render.engine.text.Fonts; import dev.loki.lovisual.render.engine.text.Fonts;
import dev.loki.lovisual.render.engine.text.TextRenderer; 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.ArrayList;
import java.util.List; import java.util.List;
import static dev.loki.lovisual.features.theme.Theme.theme; 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. * Input-field rendering: wrapping, caret, selection highlight, char counter and search chip.
*/ */
final class BetterChatInputRenderer { public final class BetterChatInputRenderer {
private 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; if (!(ClientScreen.current() instanceof ChatScreen chatScreen)) return;
EditBox field = ((ChatScreenAccessor) chatScreen).getChatField(); EditBox field = ((ChatScreenAccessor) chatScreen).getChatField();
if (field == null) return; if (field == null) return;

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0. * Licensed under the GNU General Public License v3.0.
*/ */
package dev.loki.lovisual.features.gui.chat; package dev.loki.lovisual.features.gui.chat.input;
import com.mojang.blaze3d.platform.InputConstants; import com.mojang.blaze3d.platform.InputConstants;
import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat; 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.client.gui.screens.ChatScreen;
import net.minecraft.util.Mth; import net.minecraft.util.Mth;
import org.lwjgl.glfw.GLFW; 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, * 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}. * context menu and click/selection logic. Delegates textual actions to {@link BetterChatContextMenuHandler}.
*/ */
final class BetterChatMouseClickHandler { public final class BetterChatMouseClickHandler {
private 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(); Minecraft mc = Minecraft.getInstance();
if (!BetterChat.isActive()) return false; if (!BetterChat.isActive()) return false;
if (!(ClientScreen.current() instanceof ChatScreen)) return false; if (!(ClientScreen.current() instanceof ChatScreen)) return false;

View file

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

View file

@ -5,27 +5,30 @@
* Licensed under the GNU General Public License v3.0. * 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.Collections;
import java.util.List; import java.util.List;
import static dev.loki.lovisual.features.theme.Theme.theme; 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. * Right-click context menu record + entry record, extracted from the renderer facade.
* Rendering leans on {@link BetterChatDrawUtil} and {@link BetterChatFontUtil}. * Rendering leans on {@link BetterChatDrawUtil} and {@link BetterChatFontUtil}.
*/ */
final class BetterChatContextMenuModel { public final class BetterChatContextMenuModel {
private 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_X = 8f;
private static final float MENU_PAD_Y = 7f; private static final float MENU_PAD_Y = 7f;
private static final float MENU_RADIUS = 9f; private static final float MENU_RADIUS = 9f;
static ContextMenu closed() { public static ContextMenu closed() {
return new ContextMenu(false, 0, 0, Collections.emptyList()); return new ContextMenu(false, 0, 0, Collections.emptyList());
} }
@ -37,13 +40,13 @@ final class BetterChatContextMenuModel {
return Math.max(20f, fontSize + 7f); return Math.max(20f, fontSize + 7f);
} }
boolean contains(double mx, double my) { public boolean contains(double mx, double my) {
if (!open) return false; if (!open) return false;
float fontSize = BetterChatRenderer.lastFontSizeForUi * 0.92f; float fontSize = BetterChatRenderer.lastFontSizeForUi * 0.92f;
return mx >= x && mx <= x + width(fontSize) && my >= y && my <= y + height(fontSize); 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; if (!open) return null;
float fontSize = BetterChatRenderer.lastFontSizeForUi * 0.92f; float fontSize = BetterChatRenderer.lastFontSizeForUi * 0.92f;
float itemH = itemHeight(fontSize); float itemH = itemHeight(fontSize);
@ -58,7 +61,7 @@ final class BetterChatContextMenuModel {
return null; return null;
} }
void render(float mouseX, float mouseY, float fontSize) { public void render(float mouseX, float mouseY, float fontSize) {
if (!open || entries.isEmpty()) return; if (!open || entries.isEmpty()) return;
float fs = Math.max(12f, fontSize * 0.92f); float fs = Math.max(12f, fontSize * 0.92f);
float itemH = itemHeight(fs); float itemH = itemHeight(fs);
@ -106,7 +109,7 @@ final class BetterChatContextMenuModel {
return MENU_PAD_Y * 2f + entries.size() * itemHeight(fontSize); 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. * 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.features.gui.hud.nondraggable.impl.BetterTooltips;
import dev.loki.lovisual.util.text.ChatNameUtil; import dev.loki.lovisual.util.text.ChatNameUtil;
@ -25,17 +25,22 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; 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 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. * Tooltip capture/render, timestamp formatting and post-pick hover inference.
*/ */
final class BetterChatTooltipRenderer { public final class BetterChatTooltipRenderer {
private 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()); LocalDateTime dt = LocalDateTime.ofInstant(Instant.ofEpochMilli(ms), ZoneId.systemDefault());
DateTimeFormatter fmt = DateTimeFormatter.ofPattern(withSeconds ? "HH:mm:ss" : "HH:mm"); DateTimeFormatter fmt = DateTimeFormatter.ofPattern(withSeconds ? "HH:mm:ss" : "HH:mm");
return dt.format(fmt); return dt.format(fmt);
@ -46,7 +51,7 @@ final class BetterChatTooltipRenderer {
return dt.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")); 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<>(); List<ChatHoverUtil.ColoredLine> lines = new ArrayList<>();
LocalDateTime dt = LocalDateTime.ofInstant(Instant.ofEpochMilli(ms), ZoneId.systemDefault()); LocalDateTime dt = LocalDateTime.ofInstant(Instant.ofEpochMilli(ms), ZoneId.systemDefault());
if (showDate) { if (showDate) {
@ -62,7 +67,7 @@ final class BetterChatTooltipRenderer {
return new HoverTip(lines, null, false, net.minecraft.world.item.ItemStack.EMPTY); 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; if (tip == null || ctx == null) return;
Minecraft mc = Minecraft.getInstance(); Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.getWindow() == null) return; if (mc == null || mc.getWindow() == null) return;
@ -81,7 +86,7 @@ final class BetterChatTooltipRenderer {
BetterChatRenderer.tooltipCaptured = true; BetterChatRenderer.tooltipCaptured = true;
} }
static void renderCapturedTooltip(GuiGraphicsExtractor ctx) { public static void renderCapturedTooltip(GuiGraphicsExtractor ctx) {
if (!BetterChatRenderer.tooltipCaptured) return; if (!BetterChatRenderer.tooltipCaptured) return;
BetterTooltips.renderTooltipWithContext(ctx); BetterTooltips.renderTooltipWithContext(ctx);
BetterTooltips.resetTooltipScaleOverride(); BetterTooltips.resetTooltipScaleOverride();
@ -103,7 +108,7 @@ final class BetterChatTooltipRenderer {
/** /**
* Mirror of the original post-pick hover/suggest-inference block (lines 466-518). * 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( boolean overSuggestWindow = BetterChatSuggestionState.isInsideSuggestWindow(
BetterChatRenderer.lastMouseFx, BetterChatRenderer.lastMouseFy, BetterChatRenderer.lastMouseFx, BetterChatRenderer.lastMouseFy,
BetterChatRenderer.lastMouseFx, BetterChatRenderer.lastMouseFy); BetterChatRenderer.lastMouseFx, BetterChatRenderer.lastMouseFy);

View file

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

View file

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

View file

@ -5,13 +5,16 @@
* Licensed under the GNU General Public License v3.0. * 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.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicLong; 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. * 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. * 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 com.mojang.brigadier.suggestion.Suggestion;
import dev.loki.lovisual.features.command.CommandManager; import dev.loki.lovisual.features.command.CommandManager;
import dev.loki.lovisual.features.command.CommandPrefix; 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 dev.loki.lovisual.util.screen.ClientScreen;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.EditBox; 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 net.minecraft.client.input.KeyEvent;
import java.util.List; 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. * 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. * 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 net.minecraft.network.chat.Component;
import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat; import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import dev.loki.lovisual.features.gui.chat.model.ChatLine;
public final class BetterChatStore { public final class BetterChatStore {

View file

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

View file

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

View file

@ -5,28 +5,30 @@
* Licensed under the GNU General Public License v3.0. * 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 net.minecraft.util.Mth;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; 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. * Frame layout record: builds screen-space line/bubble geometry from laid-out visual lines.
* Pure geometry + picking, no rendering or font work. * Pure geometry + picking, no rendering or font work.
*/ */
final class BetterChatFrameModel { public final class BetterChatFrameModel {
private 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() { static FrameLayout empty() {
return new FrameLayout(0, 0, 0, 0, Collections.emptyList(), Collections.emptyList()); 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()); List<BetterChatModel.FrameLine> list = new ArrayList<>(lines.size());
float cursorY = y + BetterChatRenderer.MESSAGE_PAD_Y + yOffset; float cursorY = y + BetterChatRenderer.MESSAGE_PAD_Y + yOffset;
int lastMessageGroup = Integer.MIN_VALUE; int lastMessageGroup = Integer.MIN_VALUE;
@ -90,7 +92,7 @@ final class BetterChatFrameModel {
return bubbles; return bubbles;
} }
BetterChatModel.MessageClipBounds messageClipBounds() { public BetterChatModel.MessageClipBounds messageClipBounds() {
if (lines.isEmpty() || h <= 0f) return null; if (lines.isEmpty() || h <= 0f) return null;
float clipX = x + BetterChatRenderer.PADDING; float clipX = x + BetterChatRenderer.PADDING;
float clipW = Math.max(1f, w - BetterChatRenderer.PADDING * 2f); 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; 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; if (!contains(mx, my)) return null;
for (BetterChatModel.FrameLine line : lines) { for (BetterChatModel.FrameLine line : lines) {
if (my < line.y0() || my > line.y1() + BetterChatRenderer.LINE_SPACING) continue; 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. * 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.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.text.TextRenderer; import dev.loki.lovisual.render.engine.text.TextRenderer;
import dev.loki.lovisual.render.helpers.SystemCursor; import dev.loki.lovisual.render.helpers.SystemCursor;
import net.minecraft.client.gui.GuiGraphicsExtractor; 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, * 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. * suggestion, scrollbar and tooltip phases. All real work lives in the other package-private helpers.
*/ */
final class BetterChatFrameRenderer { public final class BetterChatFrameRenderer {
private 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. * 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; import dev.loki.lovisual.features.command.CommandOutput;
@ -13,16 +13,18 @@ import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
import java.util.List; 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 * Merges command-output lines into the visible chat tail, sharing the identity-keyed
* command-line cache held on {@link BetterChatRenderer}. * command-line cache held on {@link BetterChatRenderer}.
*/ */
final class BetterChatCommandMerge { public final class BetterChatCommandMerge {
private BetterChatCommandMerge() { private BetterChatCommandMerge() {
} }
static List<ChatLine> mergeCommandOutput(List<ChatLine> storeLines, public static List<ChatLine> mergeCommandOutput(List<ChatLine> storeLines,
List<CommandOutput.MessageLine> commandLines, List<CommandOutput.MessageLine> commandLines,
int needed) { int needed) {
if (commandLines == null || commandLines.isEmpty()) { if (commandLines == null || commandLines.isEmpty()) {

View file

@ -4,22 +4,28 @@
* *
* Licensed under the GNU General Public License v3.0. * 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.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 dev.loki.lovisual.render.helpers.SystemCursor;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.components.EditBox;
import net.minecraft.util.Mth; import net.minecraft.util.Mth;
import java.util.List; import java.util.List;
import static dev.loki.lovisual.features.theme.Theme.theme; 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. * Draws the autocomplete suggestion popup above the input field each frame.
*/ */
final class BetterChatSuggestionRenderer { public final class BetterChatSuggestionRenderer {
private BetterChatSuggestionRenderer() { private BetterChatSuggestionRenderer() {
} }
static void renderSuggestions() { public static void renderSuggestions() {
BetterChatRenderer.suggestActive = false; BetterChatRenderer.suggestActive = false;
BetterChatRenderer.suggestHasScrollbar = false; BetterChatRenderer.suggestHasScrollbar = false;
if (BetterChatRenderer.storedScreen == null) return; if (BetterChatRenderer.storedScreen == null) return;

View file

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

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0. * Licensed under the GNU General Public License v3.0.
*/ */
package dev.loki.lovisual.features.gui.chat; package dev.loki.lovisual.features.gui.chat.suggestions;
import com.mojang.brigadier.context.StringRange; import com.mojang.brigadier.context.StringRange;
import com.mojang.brigadier.suggestion.Suggestion; 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.gui.screens.ChatScreen;
import net.minecraft.client.multiplayer.PlayerInfo; import net.minecraft.client.multiplayer.PlayerInfo;
import net.minecraft.client.renderer.Rect2i; import net.minecraft.client.renderer.Rect2i;
import dev.loki.lovisual.mixins.accessors.ChatInputSuggestorAccessor; import dev.loki.lovisual.mixins.accessors.chat.ChatInputSuggestorAccessor;
import dev.loki.lovisual.mixins.accessors.ChatScreenAccessor; import dev.loki.lovisual.mixins.accessors.chat.ChatScreenAccessor;
import dev.loki.lovisual.mixins.accessors.ChatScreenSuggestorAccessor; import dev.loki.lovisual.mixins.accessors.chat.ChatScreenSuggestorAccessor;
import dev.loki.lovisual.mixins.accessors.SuggestionWindowAccessor; import dev.loki.lovisual.mixins.accessors.chat.SuggestionWindowAccessor;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Locale; 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. * 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. * 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.command.CommandOutput;
import dev.loki.lovisual.render.engine.svg.SvgRenderOptions; 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 net.minecraft.network.chat.Component;
import java.util.List; 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. * Glyph + command-prefix gradient rendering for laid-out frame lines.
*/ */
final class BetterChatGlyphRenderer { public final class BetterChatGlyphRenderer {
private 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; if (lines == null || lines.isEmpty()) return;
float scale = BetterChatFontUtil.scaleForSize(fontSize); float scale = BetterChatFontUtil.scaleForSize(fontSize);
TextRenderer current = null; TextRenderer current = null;

View file

@ -4,7 +4,7 @@
* *
* Licensed under the GNU General Public License v3.0. * 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.command.CommandOutput;
import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry;
import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat; 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 net.minecraft.util.Mth;
import java.util.Collections; import java.util.Collections;
import java.util.List; 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() { private BetterChatLayoutEngine() {
} }
static void markLayoutDirty() { public static void markLayoutDirty() {
} }
static BetterChatModel.LayoutResult layoutLinesTailCached(List<ChatLine> messages, int[] messageGroups, float fontSize, float maxWidth, int maxLines) { 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); 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). * @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.ctxGui = ctx;
BetterChatRenderer.ctxBaseX = baseX; BetterChatRenderer.ctxBaseX = baseX;
BetterChatRenderer.ctxBaseY = baseY; BetterChatRenderer.ctxBaseY = baseY;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0. * Licensed under the GNU General Public License v3.0.
*/ */
package dev.loki.lovisual.features.gui.chat; package dev.loki.lovisual.features.gui.chat.text.util;
import dev.loki.lovisual.util.item.TopEnchantUtil; import dev.loki.lovisual.util.item.TopEnchantUtil;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
@ -25,6 +25,9 @@ import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; 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. * 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.animation.AnimationUtility;
import dev.loki.lovisual.render.engine.renderer.Renderer2D; import dev.loki.lovisual.render.engine.renderer.Renderer2D;
final class ClickGuiDrawHelpers { public final class ClickGuiDrawHelpers {
private 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.editor.ClickGuiTextEditorState;
import dev.loki.lovisual.features.gui.clickgui.picker.ClickGuiPickerState; 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.protocol.CombatProtocolHeuristicsEditorState;
import dev.loki.lovisual.features.gui.clickgui.settings.TextEditorOwner; import dev.loki.lovisual.features.gui.clickgui.settings.owner.TextEditorOwner;
import dev.loki.lovisual.features.gui.clickgui.settings.TextSetting; import dev.loki.lovisual.features.gui.clickgui.settings.text.TextSetting;
import dev.loki.lovisual.features.module.ModuleManager; import dev.loki.lovisual.features.module.ModuleManager;
import dev.loki.lovisual.util.text.ClipboardUtil; import dev.loki.lovisual.util.text.ClipboardUtil;
final class ClickGuiEditorManager { public final class ClickGuiEditorManager {
private ClickGuiEditorManager() { private ClickGuiEditorManager() {
} }

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