chore(history): squash 100 commit(s) from 2026-09-24
- fix(backend): case-insensitive unique email, revoke PUBLIC schema access, pin Argon2id params - test(backend): assert password length cap boundary (256 ok, 257 rejected) - docs(backend): plan — typed JWT token kinds so refresh/device tokens cannot pass as access tokens - feat(backend): JWT access/refresh token issue and verify - feat(backend): accounts repository (create/find_by_email/find_by_id) - refactor(gui): LoVisualAddonManagerScreen 989→10 файлов addon/ (8.5.2) - docs(backend): plan — fix sqlx::migrate! path in integration tests - feat(backend): POST /auth/register and /auth/login - refactor(settings): SettingsPanelComponent 715→81 + 6 helpers (8.5.2) - docs(backend): plan — harden device flow (single-use codes, bounded store, 404/429) - feat(backend): OAuth device authorization grant for mod login - fix(backend): first confirm wins for device codes - docs(backend): plan — split Task 8 (refactor) and Task 9 (avatars), harden avatar handling - refactor(mixins): LocalPlayerMixin 691→111 + 4 handlers (8.5.2) - refactor(visuals): Trails 687→130 (8.5.2) - refactor(render): ItemBatchRenderer 677->100 (8.5.2) - refactor(visuals): ReimaginedVisual 674→118 + 5 helpers (8.5.2) - refactor(config): ConfigSerializer 668→91 + 4 helpers (8.5.2) - refactor(hud): DynamicIsland 661→158 + 4 helpers (8.5.2) - refactor(render): GlStencilFramebufferSupport 666→169 (8.5.2) - refactor(gui): MenuScreen 669→128 + 4 helpers (8.5.2) - refactor(render): UiStyle 644→170 + 3 helpers (8.5.2) - refactor(gui): ModuleComponent 613→98 + 4 helpers (8.5.2) - refactor(media): MediaSessionService 616→200 + 4 helpers (8.5.2) - refactor(gui): RelationsComponent 661→59 + 4 helpers (8.5.2) - chore(license): strip GPL file headers from all Java sources - refactor(aiming): PointTracker 583→168 + 2 helpers (8.5.2) - refactor(gui): LoVisualProxyManagerScreen 591→132 + 2 helpers (8.5.2) - refactor(visuals): KillEffect 588→96 + 4 helpers (8.5.2) - refactor(render): MeshBuilder +4 helpers (8.5.2) - refactor(visuals): extract WorldParticlesRender helper (8.5.2) - refactor(world): ExplosionDamageUtil 551→116 + 2 helpers (8.5.2) - refactor(visuals): TazikHat 596->179 + Model + Palette in hats/tazik (8.5.2) - chore(license): strip GPL header from remaining 30 files and make strip script variant-aware - refactor(gui): ThemeComponent 561->166 + CardRenderer + ScrollState (8.5.2) - refactor(hud): CustomHotbar 556→178 + Renderer + Selection + SelectionGradient (8.5.2) - refactor(gui): ThemeCardRenderer perf + readability polish - refactor(clickgui): CooldownRulesSetting 596->198 + Editor + DetailRenderer (8.5.2) - refactor(hud): HudNotifier 561->200 + Painter + runtime/HudNotifierRuntime (8.5.2) - refactor(theme): Themes 555->168 + impl/Transition + impl/Blending + impl/ProfileCodec (8.5.2) - refactor(theme): EditableClickGuiTheme 205->185 + JavaDoc (8.5.2) - refactor(theme): ThemeStore 491->128 + store/ThemeStoreJson + store/ThemeStoreIO (8.5.2) - refactor(clickgui): ClickGuiRenderer 604->200 compacted one-line delegators + JavaDoc (8.5.2) - refactor(mainmenu): LoVisualMainMenuScreen 551->161 + impl/Painter + impl/Renderer + impl/TextUtil (8.5.2) - refactor(clickgui): ClickGuiTextEditorState 531->187 + impl/EditorCaret + impl/EditorPainter (8.5.2) - refactor(tab): TabListModel 525->139 + model/Collector + model/Reader + model/Signature + model/TextSplitter (8.5.2) - refactor(backend): shared bearer helper and test helpers, build_app takes Config, validate JWT secret strength - refactor(module): ModuleManager 521->198 + impl/Registrar + impl/Dispatcher (8.5.2) - feat(backend): avatar upload with decode, square crop, PNG re-encode and S3 storage - refactor(clip): ClipFunction 512->146 + impl/Geometry + impl/Debug (8.5.2) - docs(backend): implementation plans for gateway (auth hardening, gRPC, rate limits) and configs-service - refactor(iris-patch): ShaderPatchEngine 499->146 + impl/Repo (8.5.2) - chore(frontend): add router, react-query, fonts and vitest; dev proxy to gateway - refactor(hud): ScriptedListHudPanel 499->158 + panel/Props + panel/Signature (8.5.2) - refactor(hud): BaseHudElement 499->199 + impl/Registry + impl/Namer + impl/Prewarm (8.5.2) - refactor(clickgui): Setting 498->170 + impl/Localization + impl/I18n (8.5.2) - refact(viewmodel): split swing animations into camera/swing package - refact(kineticlyrics): split module into stage, playback and modes - rename(holeesp): module HoleESP -> CrystalHoles - refact(crystalholes): split module into crystal scanner, renderer and safety - refact(addonmanager): split manager into lifecycle, runtime, descriptors and profiles - refact(accountconfig): split config into store, session and value helpers - refactor(render): CustomTextRenderer 229->195, extract glyph-pass into GradientTexts helper - docs(TODO): mark AddonManager split done; close 9.2 refactor gate - refactor(media): LinuxMediaSession 441->148, split reader + track/seek state - refactor(nametags): split NameTags into facade + impl helpers - refactor(clickgui): split MainSettingsComponent into facade + scroll + model - refactor(hud): split CustomBar into facade, model and BarSettings - docs(frontend): implementation plan with design system from the mod theme - feat(frontend): design tokens from the mod theme, fonts and shared UI kit - fix(accounts): run migrations on startup, offload Argon2, validate register input, JSON error shape - docs(gateway): plan note on splitting auth handlers before refresh endpoints - feat(frontend): API client with silent refresh, error descriptions and test helpers - style(mod): group compact one-line bulk query methods in ModuleManager - feat(frontend): session restore, login and registration with client-side validation - refactor(hud): split CustomHealthBar into facade + painter + script renderer - docs(mod): record the 2026-09-24 HUD/settings split wave in TODO phase 8.5 - refactor(rhi): split GlStencilShapeClipBackend into facade + native-state + pass-lifecycle helpers - refactor(rhi): split VulkanRenderStateBridge into facade + MSAA and stencil state helpers - refactor(backtrack): split BacktrackController into facade + model + impl helpers - refactor(svg): split SvgPathParser into facade + arc geometry + command/curve helpers - refactor(mixin): split ClientPacketListenerMixin into hook-only mixin + handlers - refactor(renderer3d): un-nest batch bindings + culling into sibling impl types - refactor(renderwarp): extract static factories + geometry into impl helpers - feat(backend): add common crate with shared JWT, internal gateway contract and accounts proto - refactor(guimixin): move hook bodies into handlers, keep mixin as hooks + shadows - feat(accounts): accept only gateway traffic, read identity from gateway header - refactor(cacheduiscriptruntime): extract engine, hashing and frame stats into impl - refactor(customskyboxrenderer): extract projection, shader passes and sun into impl - refactor(betterchatstoremanager): extract persistence, key/path and hover helpers into impl - feat(accounts): rotating opaque refresh tokens in httpOnly cookie, /auth/refresh and /auth/logout - refactor(targetesp): extract crystal rendering subsystem into impl/TargetEspCrystalRenderer - refactor(betterchathovercache): extract disk codec and lookup indexing into impl/ChatHoverCacheCodec - refactor(microsoftauth): split HTTP transport, device-code and Xbox flows into impl/ - refactor(pvpcooldowns): extract local item-rule engine and defaults into impl/PvpCooldownRules - refactor(lovisual): extract HUD/world render orchestration into HudRender helper - refactor(statuseffectheuristics): extract palette/inference into ParticlePalette and color utils into ParticleColors - refactor(dropesp): extract overlay/label render subsystem into impl/DropEspOverlayRenderer - refactor(proxy): extract SOCKS handshake message builders into ProxyProtocolMessages - refactor(eagleutil): promote EdgeRecovery controller and RecoveryMode to top-level class
This commit is contained in:
parent
9d08fa910a
commit
72bc4c7148
1897 changed files with 36199 additions and 39289 deletions
17
mod/TODO.md
17
mod/TODO.md
|
|
@ -313,7 +313,9 @@
|
|||
(Batches/Bubble/Funnel) + ambient/particle/ 197 + geometry/ доработаны (offset/multiplyAlpha/STACKS) —
|
||||
типы частиц в `visuals/effects/ambient/`
|
||||
- [x] Module 989 → ГОТОВО c8da336: 1031→139 + 4×≤134 (ext/) — базовый класс: settings-механику вынести в `module/ext/` (осторожно, общий предок!)
|
||||
- [ ] LoVisualAddonManagerScreen 989 — как AltManager — как AltManager
|
||||
- [x] LoVisualAddonManagerScreen 989 → ГОТОВО de56b4b9: 989→112 + 10 файлов mainmenu/addon/
|
||||
(Controller 164, Ops 112, PanelAnims 56, auth/AddonScanState 52 + render/ 115/162/183
|
||||
+ render/cards/ 79/88), все ≤200 — по образцу AltManager
|
||||
- [x] Chams 976 → ГОТОВО 78b5335: 985→129 + 4×≤195 — режимы рендера в подклассы-политики (фасад модуля тонкий)
|
||||
- [x] ClickGuiPickerState 962 → ГОТОВО 234f0d9: 970→166 + 4×≤140; CombatProtocolHeuristicsEditorState 932→79 +4×≤192 03ae4a6 — state/actions хелперы
|
||||
- [x] ModulesMenuScreen 960 → ГОТОВО 234f0d9: 966→145 + 4×≤178 — рендер категорий отдельно от состояния
|
||||
|
|
@ -331,6 +333,12 @@
|
|||
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 и др.)
|
||||
- [x] Волна 2026-09-24 (HUD/settings): NameTags 453→223 (nametags/impl/ Keys+Collector+Projection) b1414d7a,
|
||||
MainSettingsComponent 448→291 (+Scroll+Model) 3f0d452f, LinuxMediaSession 441→148 3d11bb4e,
|
||||
CustomTextRenderer 229→195 (GradientTexts), CustomBar 438→185 (+CustomBarModel константы
|
||||
+bars/impl/BarSettings, forwardеры удалены, mixins правлены на реальные рендереры) 4e442fb7,
|
||||
CustomHealthBar 406→146 (+bars/impl/HealthBarPainter+HealthScriptRenderer, dead-code удалён) a3415435.
|
||||
bars/ закрыт: все файлы ≤200, impl/ = 3 файла.
|
||||
|
||||
### 8.5.3 201–400 строк (~140 файлов) — проход «касаться = чинить»
|
||||
- [ ] Не делать отдельным марафоном: при правке файла по другой фазе — первым коммитом
|
||||
|
|
@ -343,10 +351,15 @@
|
|||
- [ ] После каждого шага: `./gradlew build` ✓, `./gradlew test` ✓; конфиг-id модулей и
|
||||
ключи сеттингов НЕ ломаем (совместимость .lvcfg); скрипты-переименования пакетов
|
||||
не трогают string-literals id — проверять grep'ом после moves.
|
||||
- [ ] Критерий закрытия для старта 9.2: ноль файлов >200 в путях, которые трогает Optimize
|
||||
- [x] Критерий закрытия для старта 9.2: ноль файлов >200 в путях, которые трогает Optimize
|
||||
(Renderer2D-секции blur/glass, OrderedUiBatcher, HUD-имплементации bgEffect,
|
||||
MsdfFont/CustomTextRenderer), и ноль папок >4 в этих же пакетах. Полный ноль по всем
|
||||
244/86 — долгий хвост, 9.2 им не блокируется.
|
||||
→ 2026-09-24: MsdfFont 155 ✓, OrderedUiBatcher 180 ✓, CustomTextRenderer 229→195 ✓
|
||||
(glyph-pass вынесен в GradientTexts). Остаток гейта — bgEffect-HUD >200:
|
||||
TargetHud 333, Triangulator 304, Scoreboard 233 — ВСЕ фасады Фазы 3 с helper-пакетами
|
||||
(targethud/render/, triangulator/{render,Solver,Tracker,View}, panels/scoreboard/) →
|
||||
исключение по правилу фасадов. ГЕЙТ 9.2 ЗАКРЫТ.
|
||||
- [ ] Обновить этот раздел при подходе к волне (отмечать [x], доснимать счётчики).
|
||||
|
||||
## Фаза 9: Оптимизация FPS (аудит 2026-09-09) — ПЛАН
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.command.impl;
|
||||
|
||||
import dev.loki.lovisual.features.command.ClientCommand;
|
||||
|
|
|
|||
247
mod/src/main/java/dev/loki/lovisual/HudRender.java
Normal file
247
mod/src/main/java/dev/loki/lovisual/HudRender.java
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
package dev.loki.lovisual;
|
||||
|
||||
import dev.loki.lovisual.addon.pipeline.AddonRenderPipelineManager;
|
||||
import dev.loki.lovisual.api.v0.render.LoVisualRenderStage;
|
||||
import dev.loki.lovisual.features.gui.hud.anchor.HudRenderSpace;
|
||||
import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry;
|
||||
import dev.loki.lovisual.features.gui.hud.nondraggable.StaticHudElementRegistry;
|
||||
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons.SwapTooltip;
|
||||
import dev.loki.lovisual.features.module.core.ModuleManager;
|
||||
import dev.loki.lovisual.features.module.phase.HudPhase;
|
||||
import dev.loki.lovisual.features.module.phase.WorldPhase;
|
||||
import dev.loki.lovisual.mixininterface.render.IGuiGraphics;
|
||||
import dev.loki.lovisual.render.engine.core.frame.RenderPhase;
|
||||
import dev.loki.lovisual.render.engine.core.frame.RenderPhaseScope;
|
||||
import dev.loki.lovisual.render.engine.core.context.ViewportContext;
|
||||
import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem;
|
||||
import dev.loki.lovisual.render.engine.profiler.config.ProfilerPhase;
|
||||
import dev.loki.lovisual.render.engine.profiler.render.RenderProfiler2D;
|
||||
import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||
import dev.loki.lovisual.render.engine.text.render.TextRenderer;
|
||||
import dev.loki.lovisual.render.helpers.util.SystemCursor;
|
||||
import dev.loki.lovisual.render.helpers.util.TickDelta;
|
||||
import net.minecraft.client.DeltaTracker;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
|
||||
/**
|
||||
* HUD and world-phase rendering driven by the client entry point. Hosts the raw/scaled/logical
|
||||
* HUD pass orchestration that {@link LoVisual} registers against the Fabric HUD and level render
|
||||
* hooks, so behaviour stays identical to the inlined implementation.
|
||||
*/
|
||||
final class HudRender {
|
||||
private HudRender() {
|
||||
}
|
||||
|
||||
static void renderWorldPhase(WorldPhase phase,
|
||||
com.mojang.blaze3d.vertex.PoseStack matrices,
|
||||
net.minecraft.client.renderer.SubmitNodeCollector consumers) {
|
||||
if (!Lifecycle.canRunRender()) return;
|
||||
|
||||
ModuleManager.renderWorld(
|
||||
phase,
|
||||
matrices,
|
||||
consumers,
|
||||
TickDelta.tickProgress(false)
|
||||
);
|
||||
}
|
||||
|
||||
private static void refreshHudFrameTiming(DeltaTracker tickCounter) {
|
||||
LoVisualRenderSystem.updateFrameTiming(
|
||||
TickDelta.tickProgress(tickCounter, false),
|
||||
TickDelta.frameDeltaTicks(tickCounter),
|
||||
TickDelta.fixedDeltaTicks(tickCounter)
|
||||
);
|
||||
}
|
||||
|
||||
static void renderHudPhase(HudPhase phase, GuiGraphicsExtractor ctx, DeltaTracker tickCounter) {
|
||||
if (!Lifecycle.canRunHud()) return;
|
||||
float tickProgress = TickDelta.tickProgress(tickCounter, false);
|
||||
ModuleManager.renderHud(phase, ctx, tickProgress);
|
||||
Renderer2D.Deferred2DLayer layer = deferredLayerForHudPhase(phase);
|
||||
if (renderHudEngine(phase, layer, ctx, tickCounter)) {
|
||||
appendHudDeferredMarker(layer, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean renderHudEngine(HudPhase phase,
|
||||
Renderer2D.Deferred2DLayer layer,
|
||||
GuiGraphicsExtractor ctx,
|
||||
DeltaTracker tickCounter) {
|
||||
boolean rawMain = hasHudMainPassWork(phase, HudRenderSpace.UNSCALED)
|
||||
|| AddonRenderPipelineManager.hasActiveCallbacks(LoVisualRenderStage.HUD_RAW);
|
||||
boolean scaledMain = hasHudMainPassWork(phase, HudRenderSpace.SCALED)
|
||||
|| AddonRenderPipelineManager.hasActiveCallbacks(LoVisualRenderStage.HUD_SCALED);
|
||||
boolean logicalMain = hasHudMainPassWork(phase, HudRenderSpace.UNSCALED_LOGICAL)
|
||||
|| AddonRenderPipelineManager.hasActiveCallbacks(LoVisualRenderStage.HUD_LOGICAL);
|
||||
boolean rawForeground = hasHudForegroundPassWork(phase, HudRenderSpace.UNSCALED)
|
||||
|| AddonRenderPipelineManager.hasActiveCallbacks(LoVisualRenderStage.HUD_RAW_FOREGROUND);
|
||||
boolean scaledForeground = hasHudForegroundPassWork(phase, HudRenderSpace.SCALED)
|
||||
|| AddonRenderPipelineManager.hasActiveCallbacks(LoVisualRenderStage.HUD_SCALED_FOREGROUND);
|
||||
boolean logicalForeground = hasHudForegroundPassWork(phase, HudRenderSpace.UNSCALED_LOGICAL)
|
||||
|| AddonRenderPipelineManager.hasActiveCallbacks(LoVisualRenderStage.HUD_LOGICAL_FOREGROUND);
|
||||
if (!rawMain && !scaledMain && !logicalMain && !rawForeground && !scaledForeground && !logicalForeground) {
|
||||
return false;
|
||||
}
|
||||
|
||||
float tickProgress = TickDelta.tickProgress(tickCounter, false);
|
||||
TextRenderer textRenderer = TextRenderer.get();
|
||||
|
||||
FullScreenRenderer.ensureInit();
|
||||
ViewportContext.beginUnscaled(ctx);
|
||||
refreshHudFrameTiming(tickCounter);
|
||||
|
||||
Renderer2D.withDeferredLayer(layer, () -> {
|
||||
SystemCursor.beginFrame(ctx);
|
||||
try (RenderPhaseScope hudPhase = LoVisualRenderSystem.phase(RenderPhase.HUD_MAIN, "2d:phase:" + phase.name())) {
|
||||
// Raw HUD pass
|
||||
if (rawMain) {
|
||||
Renderer2D.COLOR.begin();
|
||||
ModuleManager.renderHudEngine(phase, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
StaticHudElementRegistry.renderAllEngine(phase, HudRenderSpace.UNSCALED, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
AddonRenderPipelineManager.render2D(LoVisualRenderStage.HUD_RAW, phase, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
Renderer2D.COLOR.render();
|
||||
}
|
||||
|
||||
// Scaled fixed HUD pass
|
||||
if (scaledMain) {
|
||||
ViewportContext.beginScaled(ctx);
|
||||
Renderer2D.COLOR.begin();
|
||||
StaticHudElementRegistry.renderAllEngine(phase, HudRenderSpace.SCALED, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
AddonRenderPipelineManager.render2D(LoVisualRenderStage.HUD_SCALED, phase, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
Renderer2D.COLOR.render();
|
||||
}
|
||||
|
||||
// Logical HUD pass
|
||||
if (logicalMain) {
|
||||
ViewportContext.beginUnscaledLogical(ctx);
|
||||
Renderer2D.COLOR.begin();
|
||||
StaticHudElementRegistry.renderAllEngine(phase, HudRenderSpace.UNSCALED_LOGICAL, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
DraggableHudElementRegistry.renderAllEngine(phase, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
AddonRenderPipelineManager.render2D(LoVisualRenderStage.HUD_LOGICAL, phase, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
Renderer2D.COLOR.render();
|
||||
}
|
||||
|
||||
// Raw foreground
|
||||
if (rawForeground) {
|
||||
ViewportContext.beginUnscaled(ctx);
|
||||
Renderer2D.COLOR.begin();
|
||||
ModuleManager.renderHudEngineForeground(phase, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
StaticHudElementRegistry.renderAllEngineForeground(phase, HudRenderSpace.UNSCALED, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
AddonRenderPipelineManager.render2D(LoVisualRenderStage.HUD_RAW_FOREGROUND, phase, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
Renderer2D.COLOR.render();
|
||||
}
|
||||
|
||||
// Scaled fixed HUD foreground
|
||||
if (scaledForeground) {
|
||||
ViewportContext.beginScaled(ctx);
|
||||
Renderer2D.COLOR.begin();
|
||||
StaticHudElementRegistry.renderAllEngineForeground(phase, HudRenderSpace.SCALED, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
AddonRenderPipelineManager.render2D(LoVisualRenderStage.HUD_SCALED_FOREGROUND, phase, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
Renderer2D.COLOR.render();
|
||||
}
|
||||
|
||||
// Logical foreground
|
||||
if (logicalForeground) {
|
||||
ViewportContext.beginUnscaledLogical(ctx);
|
||||
Renderer2D.COLOR.begin();
|
||||
StaticHudElementRegistry.renderAllEngineForeground(phase, HudRenderSpace.UNSCALED_LOGICAL, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
DraggableHudElementRegistry.renderAllEngineForeground(phase, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
AddonRenderPipelineManager.render2D(LoVisualRenderStage.HUD_LOGICAL_FOREGROUND, phase, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
Renderer2D.COLOR.render();
|
||||
}
|
||||
} finally {
|
||||
SystemCursor.endFrame();
|
||||
ViewportContext.end(ctx);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
static void renderHudAfterHotbar(GuiGraphicsExtractor ctx, DeltaTracker tickCounter) {
|
||||
if (!Lifecycle.canRunHud()) return;
|
||||
boolean swapTooltipWork = SwapTooltip.hasWork();
|
||||
boolean targetHudWork = DraggableHudElementRegistry.hasEngineWidgetWork("target_hud");
|
||||
if (!swapTooltipWork && !targetHudWork) return;
|
||||
|
||||
float tickProgress = TickDelta.tickProgress(tickCounter, false);
|
||||
TextRenderer textRenderer = TextRenderer.get();
|
||||
Renderer2D.Deferred2DLayer layer = Renderer2D.Deferred2DLayer.HUD_AFTER_MISC_OVERLAYS;
|
||||
|
||||
FullScreenRenderer.ensureInit();
|
||||
ViewportContext.beginUnscaled(ctx);
|
||||
refreshHudFrameTiming(tickCounter);
|
||||
|
||||
Renderer2D.withDeferredLayer(layer, () -> {
|
||||
SystemCursor.beginFrame(ctx);
|
||||
try (RenderPhaseScope hudPhase = LoVisualRenderSystem.phase(RenderPhase.HUD_EFFECTS, "2d:after_hotbar")) {
|
||||
// Raw pass
|
||||
if (swapTooltipWork) {
|
||||
try (ProfilerPhase.Scope tooltipsScope = ProfilerPhase.scope("2d:after_hotbar:swap_tooltip");
|
||||
RenderProfiler2D.Section ignoredTooltips = RenderProfiler2D.section("swap_tooltip")) {
|
||||
Renderer2D.COLOR.begin();
|
||||
SwapTooltip.render();
|
||||
Renderer2D.COLOR.render();
|
||||
}
|
||||
}
|
||||
|
||||
// Logical pass
|
||||
if (targetHudWork) {
|
||||
ViewportContext.beginUnscaledLogical(ctx);
|
||||
try (ProfilerPhase.Scope targetHudScope = ProfilerPhase.scope("2d:after_hotbar:target_hud");
|
||||
RenderProfiler2D.Section ignoredTargetHud = RenderProfiler2D.section("target_hud")) {
|
||||
Renderer2D.COLOR.begin();
|
||||
DraggableHudElementRegistry.renderEngineWidget(
|
||||
"target_hud",
|
||||
Renderer2D.COLOR,
|
||||
textRenderer,
|
||||
ctx,
|
||||
tickProgress
|
||||
);
|
||||
Renderer2D.COLOR.render();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
SystemCursor.endFrame();
|
||||
ViewportContext.end(ctx);
|
||||
}
|
||||
});
|
||||
appendHudDeferredMarker(layer, ctx);
|
||||
}
|
||||
|
||||
private static void appendHudDeferredMarker(Renderer2D.Deferred2DLayer layer, GuiGraphicsExtractor ctx) {
|
||||
if (!(ctx instanceof IGuiGraphics graphics)) return;
|
||||
ctx.nextStratum();
|
||||
graphics.lovisual$addDeferredHudMarker(layer);
|
||||
ctx.nextStratum();
|
||||
}
|
||||
|
||||
private static Renderer2D.Deferred2DLayer deferredLayerForHudPhase(HudPhase phase) {
|
||||
if (phase == null) return Renderer2D.Deferred2DLayer.BEFORE_VANILLA_GUI;
|
||||
return switch (phase) {
|
||||
case NONE -> Renderer2D.Deferred2DLayer.BEFORE_VANILLA_GUI;
|
||||
case FIRST -> Renderer2D.Deferred2DLayer.HUD_FIRST;
|
||||
case BEFORE_MISC_OVERLAYS -> Renderer2D.Deferred2DLayer.HUD_BEFORE_MISC_OVERLAYS;
|
||||
case AFTER_MISC_OVERLAYS -> Renderer2D.Deferred2DLayer.HUD_AFTER_MISC_OVERLAYS;
|
||||
case AFTER_BOSS_BAR -> Renderer2D.Deferred2DLayer.HUD_AFTER_BOSS_BAR;
|
||||
case BEFORE_DEMO_TIMER -> Renderer2D.Deferred2DLayer.HUD_BEFORE_DEMO_TIMER;
|
||||
case BEFORE_CHAT -> Renderer2D.Deferred2DLayer.HUD_BEFORE_CHAT;
|
||||
case AFTER_SUBTITLES -> Renderer2D.Deferred2DLayer.HUD_AFTER_SUBTITLES;
|
||||
case LAST -> Renderer2D.Deferred2DLayer.HUD_LAST;
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean hasHudMainPassWork(HudPhase phase, HudRenderSpace space) {
|
||||
boolean moduleWork = space == HudRenderSpace.UNSCALED && ModuleManager.hasHudEngineWork(phase);
|
||||
boolean staticWork = StaticHudElementRegistry.hasEngineWork(phase, space, false);
|
||||
boolean draggableWork = space == HudRenderSpace.UNSCALED_LOGICAL && DraggableHudElementRegistry.hasEngineWork(phase, false);
|
||||
return moduleWork || staticWork || draggableWork;
|
||||
}
|
||||
|
||||
private static boolean hasHudForegroundPassWork(HudPhase phase, HudRenderSpace space) {
|
||||
boolean moduleWork = space == HudRenderSpace.UNSCALED && ModuleManager.hasHudEngineForegroundWork(phase);
|
||||
boolean staticWork = StaticHudElementRegistry.hasEngineWork(phase, space, true);
|
||||
boolean draggableWork = space == HudRenderSpace.UNSCALED_LOGICAL && DraggableHudElementRegistry.hasEngineWork(phase, true);
|
||||
return moduleWork || staticWork || draggableWork;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import dev.loki.lovisual.util.logging.DebugLog;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import dev.loki.lovisual.events.impl.render.GameTickEvent;
|
||||
|
|
@ -14,12 +7,8 @@ import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
|||
import net.fabricmc.fabric.api.client.rendering.v1.hud.HudElementRegistry;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.hud.VanillaHudElements;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderEvents;
|
||||
import net.minecraft.client.DeltaTracker;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import dev.loki.lovisual.addon.core.AddonManager;
|
||||
import dev.loki.lovisual.addon.pipeline.AddonRenderPipelineManager;
|
||||
import dev.loki.lovisual.api.v0.render.LoVisualRenderStage;
|
||||
import dev.loki.lovisual.config.i18n.MainConfig;
|
||||
import dev.loki.lovisual.events.Events;
|
||||
import dev.loki.lovisual.features.account.AccountConfig;
|
||||
|
|
@ -27,33 +16,19 @@ import dev.loki.lovisual.features.command.core.CommandManager;
|
|||
import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer;
|
||||
import dev.loki.lovisual.features.gui.clickgui.settings.i18n.I18nPreflightContributors;
|
||||
import dev.loki.lovisual.features.gui.hud.registry.HudElements;
|
||||
import dev.loki.lovisual.features.gui.hud.anchor.HudRenderSpace;
|
||||
import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry;
|
||||
import dev.loki.lovisual.features.gui.hud.nondraggable.StaticHudElementBootstrap;
|
||||
import dev.loki.lovisual.features.gui.hud.nondraggable.StaticHudElementRegistry;
|
||||
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons.SwapTooltip;
|
||||
import dev.loki.lovisual.features.module.phase.HudPhase;
|
||||
import dev.loki.lovisual.features.module.lifecycle.ModuleAutoLoader;
|
||||
import dev.loki.lovisual.features.module.core.ModuleManager;
|
||||
import dev.loki.lovisual.features.module.phase.WorldPhase;
|
||||
import dev.loki.lovisual.mixininterface.render.IGuiGraphics;
|
||||
import dev.loki.lovisual.render.effects.NetherPortalRiftPass;
|
||||
import dev.loki.lovisual.render.effects.SleepOverlayPass;
|
||||
import dev.loki.lovisual.render.engine.LoVisualRenderEngineBootstrap;
|
||||
import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem;
|
||||
import dev.loki.lovisual.render.engine.core.frame.RenderPhase;
|
||||
import dev.loki.lovisual.render.engine.core.frame.RenderPhaseScope;
|
||||
import dev.loki.lovisual.render.engine.core.context.ViewportContext;
|
||||
import dev.loki.lovisual.render.engine.postprocess.post.PostProcessManager;
|
||||
import dev.loki.lovisual.render.engine.profiler.config.ProfilerPhase;
|
||||
import dev.loki.lovisual.render.engine.profiler.render.RenderProfiler2D;
|
||||
import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer;
|
||||
import dev.loki.lovisual.render.engine.prewarm.RenderPrewarmContributors;
|
||||
import dev.loki.lovisual.render.engine.prewarm.RenderPrewarmManager;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||
import dev.loki.lovisual.render.engine.text.render.TextRenderer;
|
||||
import dev.loki.lovisual.render.helpers.util.SystemCursor;
|
||||
import dev.loki.lovisual.render.helpers.util.TickDelta;
|
||||
import dev.loki.lovisual.util.aiming.rotation.RotationManager;
|
||||
import dev.loki.lovisual.util.block.bed.SelfBedTracker;
|
||||
import dev.loki.lovisual.util.click.AttackPressing;
|
||||
|
|
@ -71,223 +46,9 @@ import dev.loki.lovisual.util.pvp.tracker.PvpTargetTracker;
|
|||
import dev.loki.lovisual.util.pvp.tracker.PvpTracker;
|
||||
import dev.loki.lovisual.util.target.TargetManager;
|
||||
import dev.loki.lovisual.util.time.TimerController;
|
||||
import dev.loki.lovisual.features.gui.clickgui.settings.bind.KeyBindSetting;
|
||||
import dev.loki.lovisual.features.module.core.Module;
|
||||
|
||||
public class LoVisual implements ClientModInitializer {
|
||||
|
||||
private static void renderWorldPhase(WorldPhase phase,
|
||||
com.mojang.blaze3d.vertex.PoseStack matrices,
|
||||
net.minecraft.client.renderer.SubmitNodeCollector consumers) {
|
||||
if (!Lifecycle.canRunRender()) return;
|
||||
|
||||
ModuleManager.renderWorld(
|
||||
phase,
|
||||
matrices,
|
||||
consumers,
|
||||
TickDelta.tickProgress(false)
|
||||
);
|
||||
}
|
||||
|
||||
private static void refreshHudFrameTiming(DeltaTracker tickCounter) {
|
||||
LoVisualRenderSystem.updateFrameTiming(
|
||||
TickDelta.tickProgress(tickCounter, false),
|
||||
TickDelta.frameDeltaTicks(tickCounter),
|
||||
TickDelta.fixedDeltaTicks(tickCounter)
|
||||
);
|
||||
}
|
||||
|
||||
private static void renderHudPhase(HudPhase phase, GuiGraphicsExtractor ctx, DeltaTracker tickCounter) {
|
||||
if (!Lifecycle.canRunHud()) return;
|
||||
float tickProgress = TickDelta.tickProgress(tickCounter, false);
|
||||
ModuleManager.renderHud(phase, ctx, tickProgress);
|
||||
Renderer2D.Deferred2DLayer layer = deferredLayerForHudPhase(phase);
|
||||
if (renderHudEngine(phase, layer, ctx, tickCounter)) {
|
||||
appendHudDeferredMarker(layer, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean renderHudEngine(HudPhase phase,
|
||||
Renderer2D.Deferred2DLayer layer,
|
||||
GuiGraphicsExtractor ctx,
|
||||
DeltaTracker tickCounter) {
|
||||
boolean rawMain = hasHudMainPassWork(phase, HudRenderSpace.UNSCALED)
|
||||
|| AddonRenderPipelineManager.hasActiveCallbacks(LoVisualRenderStage.HUD_RAW);
|
||||
boolean scaledMain = hasHudMainPassWork(phase, HudRenderSpace.SCALED)
|
||||
|| AddonRenderPipelineManager.hasActiveCallbacks(LoVisualRenderStage.HUD_SCALED);
|
||||
boolean logicalMain = hasHudMainPassWork(phase, HudRenderSpace.UNSCALED_LOGICAL)
|
||||
|| AddonRenderPipelineManager.hasActiveCallbacks(LoVisualRenderStage.HUD_LOGICAL);
|
||||
boolean rawForeground = hasHudForegroundPassWork(phase, HudRenderSpace.UNSCALED)
|
||||
|| AddonRenderPipelineManager.hasActiveCallbacks(LoVisualRenderStage.HUD_RAW_FOREGROUND);
|
||||
boolean scaledForeground = hasHudForegroundPassWork(phase, HudRenderSpace.SCALED)
|
||||
|| AddonRenderPipelineManager.hasActiveCallbacks(LoVisualRenderStage.HUD_SCALED_FOREGROUND);
|
||||
boolean logicalForeground = hasHudForegroundPassWork(phase, HudRenderSpace.UNSCALED_LOGICAL)
|
||||
|| AddonRenderPipelineManager.hasActiveCallbacks(LoVisualRenderStage.HUD_LOGICAL_FOREGROUND);
|
||||
if (!rawMain && !scaledMain && !logicalMain && !rawForeground && !scaledForeground && !logicalForeground) {
|
||||
return false;
|
||||
}
|
||||
|
||||
float tickProgress = TickDelta.tickProgress(tickCounter, false);
|
||||
TextRenderer textRenderer = TextRenderer.get();
|
||||
|
||||
FullScreenRenderer.ensureInit();
|
||||
ViewportContext.beginUnscaled(ctx);
|
||||
refreshHudFrameTiming(tickCounter);
|
||||
|
||||
Renderer2D.withDeferredLayer(layer, () -> {
|
||||
SystemCursor.beginFrame(ctx);
|
||||
try (RenderPhaseScope hudPhase = LoVisualRenderSystem.phase(RenderPhase.HUD_MAIN, "2d:phase:" + phase.name())) {
|
||||
// Raw HUD pass
|
||||
if (rawMain) {
|
||||
Renderer2D.COLOR.begin();
|
||||
ModuleManager.renderHudEngine(phase, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
StaticHudElementRegistry.renderAllEngine(phase, HudRenderSpace.UNSCALED, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
AddonRenderPipelineManager.render2D(LoVisualRenderStage.HUD_RAW, phase, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
Renderer2D.COLOR.render();
|
||||
}
|
||||
|
||||
// Scaled fixed HUD pass
|
||||
if (scaledMain) {
|
||||
ViewportContext.beginScaled(ctx);
|
||||
Renderer2D.COLOR.begin();
|
||||
StaticHudElementRegistry.renderAllEngine(phase, HudRenderSpace.SCALED, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
AddonRenderPipelineManager.render2D(LoVisualRenderStage.HUD_SCALED, phase, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
Renderer2D.COLOR.render();
|
||||
}
|
||||
|
||||
// Logical HUD pass
|
||||
if (logicalMain) {
|
||||
ViewportContext.beginUnscaledLogical(ctx);
|
||||
Renderer2D.COLOR.begin();
|
||||
StaticHudElementRegistry.renderAllEngine(phase, HudRenderSpace.UNSCALED_LOGICAL, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
DraggableHudElementRegistry.renderAllEngine(phase, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
AddonRenderPipelineManager.render2D(LoVisualRenderStage.HUD_LOGICAL, phase, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
Renderer2D.COLOR.render();
|
||||
}
|
||||
|
||||
// Raw foreground
|
||||
if (rawForeground) {
|
||||
ViewportContext.beginUnscaled(ctx);
|
||||
Renderer2D.COLOR.begin();
|
||||
ModuleManager.renderHudEngineForeground(phase, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
StaticHudElementRegistry.renderAllEngineForeground(phase, HudRenderSpace.UNSCALED, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
AddonRenderPipelineManager.render2D(LoVisualRenderStage.HUD_RAW_FOREGROUND, phase, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
Renderer2D.COLOR.render();
|
||||
}
|
||||
|
||||
// Scaled fixed HUD foreground
|
||||
if (scaledForeground) {
|
||||
ViewportContext.beginScaled(ctx);
|
||||
Renderer2D.COLOR.begin();
|
||||
StaticHudElementRegistry.renderAllEngineForeground(phase, HudRenderSpace.SCALED, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
AddonRenderPipelineManager.render2D(LoVisualRenderStage.HUD_SCALED_FOREGROUND, phase, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
Renderer2D.COLOR.render();
|
||||
}
|
||||
|
||||
// Logical foreground
|
||||
if (logicalForeground) {
|
||||
ViewportContext.beginUnscaledLogical(ctx);
|
||||
Renderer2D.COLOR.begin();
|
||||
StaticHudElementRegistry.renderAllEngineForeground(phase, HudRenderSpace.UNSCALED_LOGICAL, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
DraggableHudElementRegistry.renderAllEngineForeground(phase, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
AddonRenderPipelineManager.render2D(LoVisualRenderStage.HUD_LOGICAL_FOREGROUND, phase, Renderer2D.COLOR, textRenderer, ctx, tickProgress);
|
||||
Renderer2D.COLOR.render();
|
||||
}
|
||||
} finally {
|
||||
SystemCursor.endFrame();
|
||||
ViewportContext.end(ctx);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void renderHudAfterHotbar(GuiGraphicsExtractor ctx, DeltaTracker tickCounter) {
|
||||
if (!Lifecycle.canRunHud()) return;
|
||||
boolean swapTooltipWork = SwapTooltip.hasWork();
|
||||
boolean targetHudWork = DraggableHudElementRegistry.hasEngineWidgetWork("target_hud");
|
||||
if (!swapTooltipWork && !targetHudWork) return;
|
||||
|
||||
float tickProgress = TickDelta.tickProgress(tickCounter, false);
|
||||
TextRenderer textRenderer = TextRenderer.get();
|
||||
Renderer2D.Deferred2DLayer layer = Renderer2D.Deferred2DLayer.HUD_AFTER_MISC_OVERLAYS;
|
||||
|
||||
FullScreenRenderer.ensureInit();
|
||||
ViewportContext.beginUnscaled(ctx);
|
||||
refreshHudFrameTiming(tickCounter);
|
||||
|
||||
Renderer2D.withDeferredLayer(layer, () -> {
|
||||
SystemCursor.beginFrame(ctx);
|
||||
try (RenderPhaseScope hudPhase = LoVisualRenderSystem.phase(RenderPhase.HUD_EFFECTS, "2d:after_hotbar")) {
|
||||
// Raw pass
|
||||
if (swapTooltipWork) {
|
||||
try (ProfilerPhase.Scope tooltipsScope = ProfilerPhase.scope("2d:after_hotbar:swap_tooltip");
|
||||
RenderProfiler2D.Section ignoredTooltips = RenderProfiler2D.section("swap_tooltip")) {
|
||||
Renderer2D.COLOR.begin();
|
||||
SwapTooltip.render();
|
||||
Renderer2D.COLOR.render();
|
||||
}
|
||||
}
|
||||
|
||||
// Logical pass
|
||||
if (targetHudWork) {
|
||||
ViewportContext.beginUnscaledLogical(ctx);
|
||||
try (ProfilerPhase.Scope targetHudScope = ProfilerPhase.scope("2d:after_hotbar:target_hud");
|
||||
RenderProfiler2D.Section ignoredTargetHud = RenderProfiler2D.section("target_hud")) {
|
||||
Renderer2D.COLOR.begin();
|
||||
DraggableHudElementRegistry.renderEngineWidget(
|
||||
"target_hud",
|
||||
Renderer2D.COLOR,
|
||||
textRenderer,
|
||||
ctx,
|
||||
tickProgress
|
||||
);
|
||||
Renderer2D.COLOR.render();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
SystemCursor.endFrame();
|
||||
ViewportContext.end(ctx);
|
||||
}
|
||||
});
|
||||
appendHudDeferredMarker(layer, ctx);
|
||||
}
|
||||
|
||||
private static void appendHudDeferredMarker(Renderer2D.Deferred2DLayer layer, GuiGraphicsExtractor ctx) {
|
||||
if (!(ctx instanceof IGuiGraphics graphics)) return;
|
||||
ctx.nextStratum();
|
||||
graphics.lovisual$addDeferredHudMarker(layer);
|
||||
ctx.nextStratum();
|
||||
}
|
||||
|
||||
private static Renderer2D.Deferred2DLayer deferredLayerForHudPhase(HudPhase phase) {
|
||||
if (phase == null) return Renderer2D.Deferred2DLayer.BEFORE_VANILLA_GUI;
|
||||
return switch (phase) {
|
||||
case NONE -> Renderer2D.Deferred2DLayer.BEFORE_VANILLA_GUI;
|
||||
case FIRST -> Renderer2D.Deferred2DLayer.HUD_FIRST;
|
||||
case BEFORE_MISC_OVERLAYS -> Renderer2D.Deferred2DLayer.HUD_BEFORE_MISC_OVERLAYS;
|
||||
case AFTER_MISC_OVERLAYS -> Renderer2D.Deferred2DLayer.HUD_AFTER_MISC_OVERLAYS;
|
||||
case AFTER_BOSS_BAR -> Renderer2D.Deferred2DLayer.HUD_AFTER_BOSS_BAR;
|
||||
case BEFORE_DEMO_TIMER -> Renderer2D.Deferred2DLayer.HUD_BEFORE_DEMO_TIMER;
|
||||
case BEFORE_CHAT -> Renderer2D.Deferred2DLayer.HUD_BEFORE_CHAT;
|
||||
case AFTER_SUBTITLES -> Renderer2D.Deferred2DLayer.HUD_AFTER_SUBTITLES;
|
||||
case LAST -> Renderer2D.Deferred2DLayer.HUD_LAST;
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean hasHudMainPassWork(HudPhase phase, HudRenderSpace space) {
|
||||
boolean moduleWork = space == HudRenderSpace.UNSCALED && ModuleManager.hasHudEngineWork(phase);
|
||||
boolean staticWork = StaticHudElementRegistry.hasEngineWork(phase, space, false);
|
||||
boolean draggableWork = space == HudRenderSpace.UNSCALED_LOGICAL && DraggableHudElementRegistry.hasEngineWork(phase, false);
|
||||
return moduleWork || staticWork || draggableWork;
|
||||
}
|
||||
|
||||
private static boolean hasHudForegroundPassWork(HudPhase phase, HudRenderSpace space) {
|
||||
boolean moduleWork = space == HudRenderSpace.UNSCALED && ModuleManager.hasHudEngineForegroundWork(phase);
|
||||
boolean staticWork = StaticHudElementRegistry.hasEngineWork(phase, space, true);
|
||||
boolean draggableWork = space == HudRenderSpace.UNSCALED_LOGICAL && DraggableHudElementRegistry.hasEngineWork(phase, true);
|
||||
return moduleWork || staticWork || draggableWork;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
LoVisualRenderEngineBootstrap.init();
|
||||
|
|
@ -352,25 +113,25 @@ public class LoVisual implements ClientModInitializer {
|
|||
|
||||
/* ====================== WORLD RENDER ====================== */
|
||||
|
||||
LevelRenderEvents.COLLECT_SUBMITS.register(ctx -> renderWorldPhase(
|
||||
LevelRenderEvents.COLLECT_SUBMITS.register(ctx -> HudRender.renderWorldPhase(
|
||||
WorldPhase.BEFORE_ENTITIES,
|
||||
ctx.poseStack(),
|
||||
ctx.submitNodeCollector()
|
||||
));
|
||||
|
||||
LevelRenderEvents.AFTER_SOLID_FEATURES.register(ctx -> renderWorldPhase(
|
||||
LevelRenderEvents.AFTER_SOLID_FEATURES.register(ctx -> HudRender.renderWorldPhase(
|
||||
WorldPhase.AFTER_ENTITIES,
|
||||
ctx.poseStack(),
|
||||
ctx.submitNodeCollector()
|
||||
));
|
||||
|
||||
LevelRenderEvents.BEFORE_TRANSLUCENT_TERRAIN.register(ctx -> renderWorldPhase(
|
||||
LevelRenderEvents.BEFORE_TRANSLUCENT_TERRAIN.register(ctx -> HudRender.renderWorldPhase(
|
||||
WorldPhase.BEFORE_TRANSLUCENT,
|
||||
ctx.poseStack(),
|
||||
ctx.submitNodeCollector()
|
||||
));
|
||||
|
||||
LevelRenderEvents.END_MAIN.register(ctx -> renderWorldPhase(
|
||||
LevelRenderEvents.END_MAIN.register(ctx -> HudRender.renderWorldPhase(
|
||||
WorldPhase.END_MAIN,
|
||||
ctx.poseStack(),
|
||||
ctx.submitNodeCollector()
|
||||
|
|
@ -380,55 +141,55 @@ public class LoVisual implements ClientModInitializer {
|
|||
|
||||
HudElementRegistry.addFirst(
|
||||
Identifier.fromNamespaceAndPath("lovisual", "hud_first"),
|
||||
(ctx, tc) -> renderHudPhase(HudPhase.FIRST, ctx, tc)
|
||||
(ctx, tc) -> HudRender.renderHudPhase(HudPhase.FIRST, ctx, tc)
|
||||
);
|
||||
|
||||
HudElementRegistry.addLast(
|
||||
Identifier.fromNamespaceAndPath("lovisual", "hud_last"),
|
||||
(ctx, tc) -> renderHudPhase(HudPhase.LAST, ctx, tc)
|
||||
(ctx, tc) -> HudRender.renderHudPhase(HudPhase.LAST, ctx, tc)
|
||||
);
|
||||
|
||||
// остальные позиции HUD:
|
||||
HudElementRegistry.attachElementBefore(
|
||||
net.fabricmc.fabric.api.client.rendering.v1.hud.VanillaHudElements.MISC_OVERLAYS,
|
||||
Identifier.fromNamespaceAndPath("lovisual", "hud_before_misc"),
|
||||
(ctx, tc) -> renderHudPhase(HudPhase.BEFORE_MISC_OVERLAYS, ctx, tc)
|
||||
(ctx, tc) -> HudRender.renderHudPhase(HudPhase.BEFORE_MISC_OVERLAYS, ctx, tc)
|
||||
);
|
||||
|
||||
HudElementRegistry.attachElementAfter(
|
||||
net.fabricmc.fabric.api.client.rendering.v1.hud.VanillaHudElements.MISC_OVERLAYS,
|
||||
Identifier.fromNamespaceAndPath("lovisual", "hud_after_misc"),
|
||||
(ctx, tc) -> renderHudPhase(HudPhase.AFTER_MISC_OVERLAYS, ctx, tc)
|
||||
(ctx, tc) -> HudRender.renderHudPhase(HudPhase.AFTER_MISC_OVERLAYS, ctx, tc)
|
||||
);
|
||||
|
||||
HudElementRegistry.attachElementAfter(
|
||||
net.fabricmc.fabric.api.client.rendering.v1.hud.VanillaHudElements.SLEEP,
|
||||
Identifier.fromNamespaceAndPath("lovisual", "hud_after_boss"),
|
||||
(ctx, tc) -> renderHudPhase(HudPhase.AFTER_BOSS_BAR, ctx, tc)
|
||||
(ctx, tc) -> HudRender.renderHudPhase(HudPhase.AFTER_BOSS_BAR, ctx, tc)
|
||||
);
|
||||
|
||||
HudElementRegistry.attachElementBefore(
|
||||
net.fabricmc.fabric.api.client.rendering.v1.hud.VanillaHudElements.DEMO_TIMER,
|
||||
Identifier.fromNamespaceAndPath("lovisual", "hud_before_demo"),
|
||||
(ctx, tc) -> renderHudPhase(HudPhase.BEFORE_DEMO_TIMER, ctx, tc)
|
||||
(ctx, tc) -> HudRender.renderHudPhase(HudPhase.BEFORE_DEMO_TIMER, ctx, tc)
|
||||
);
|
||||
|
||||
HudElementRegistry.attachElementBefore(
|
||||
net.fabricmc.fabric.api.client.rendering.v1.hud.VanillaHudElements.CHAT,
|
||||
Identifier.fromNamespaceAndPath("lovisual", "hud_before_chat"),
|
||||
(ctx, tc) -> renderHudPhase(HudPhase.BEFORE_CHAT, ctx, tc)
|
||||
(ctx, tc) -> HudRender.renderHudPhase(HudPhase.BEFORE_CHAT, ctx, tc)
|
||||
);
|
||||
|
||||
HudElementRegistry.attachElementAfter(
|
||||
net.fabricmc.fabric.api.client.rendering.v1.hud.VanillaHudElements.SUBTITLES,
|
||||
Identifier.fromNamespaceAndPath("lovisual", "hud_after_subtitles"),
|
||||
(ctx, tc) -> renderHudPhase(HudPhase.AFTER_SUBTITLES, ctx, tc)
|
||||
(ctx, tc) -> HudRender.renderHudPhase(HudPhase.AFTER_SUBTITLES, ctx, tc)
|
||||
);
|
||||
|
||||
HudElementRegistry.attachElementAfter(
|
||||
VanillaHudElements.HOTBAR,
|
||||
Identifier.fromNamespaceAndPath("lovisual", "hud_after_hotbar"),
|
||||
(ctx, tc) -> renderHudAfterHotbar(ctx, tc)
|
||||
(ctx, tc) -> HudRender.renderHudAfterHotbar(ctx, tc)
|
||||
);
|
||||
|
||||
Lifecycle.activate();
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.addon.context;
|
||||
|
||||
import dev.loki.lovisual.api.v0.addon.LoVisualAddonContext;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.addon.context;
|
||||
|
||||
import dev.loki.lovisual.api.v0.addon.LoVisualAddonRuntimeContext;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.addon.context;
|
||||
|
||||
import dev.loki.lovisual.api.v0.addon.LoVisualModuleLoadContext;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.addon.context;
|
||||
|
||||
import dev.loki.lovisual.api.v0.module.ModuleExtensionContext;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.addon.core;
|
||||
|
||||
import java.util.List;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.addon.core;
|
||||
|
||||
public record AddonIssue(
|
||||
|
|
|
|||
|
|
@ -1,24 +1,9 @@
|
|||
/*
|
||||
* 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.addon.core;
|
||||
|
||||
import dev.loki.lovisual.api.v0.addon.LoVisualAddon;
|
||||
import dev.loki.lovisual.features.gui.hud.base.AbstractHudElement;
|
||||
import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement;
|
||||
import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry;
|
||||
import dev.loki.lovisual.features.gui.hud.nondraggable.StaticHudElementRegistry;
|
||||
import dev.loki.lovisual.features.module.core.Module;
|
||||
import dev.loki.lovisual.addon.pipeline.AddonSnapshot;
|
||||
import dev.loki.lovisual.addon.pipeline.AddonStateStore;
|
||||
import dev.loki.lovisual.addon.pipeline.AddonStatus;
|
||||
import dev.loki.lovisual.features.module.lifecycle.ModuleAutoLoader;
|
||||
import dev.loki.lovisual.features.module.core.ModuleManager;
|
||||
import dev.loki.lovisual.util.logging.DebugLog;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.fabricmc.loader.api.metadata.CustomValue;
|
||||
import net.fabricmc.loader.api.metadata.ModMetadata;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
|
|
@ -26,30 +11,28 @@ import java.util.LinkedHashMap;
|
|||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import dev.loki.lovisual.addon.context.LoVisualAddonContextImpl;
|
||||
import dev.loki.lovisual.addon.context.LoVisualAddonRuntimeContextImpl;
|
||||
import dev.loki.lovisual.addon.context.LoVisualModuleLoadContextImpl;
|
||||
import dev.loki.lovisual.addon.core.AddonDescriptor;
|
||||
import dev.loki.lovisual.addon.core.AddonIssue;
|
||||
import dev.loki.lovisual.addon.core.AddonRegistration;
|
||||
import dev.loki.lovisual.addon.pipeline.AddonSnapshot;
|
||||
import dev.loki.lovisual.addon.pipeline.AddonStateStore;
|
||||
import dev.loki.lovisual.addon.pipeline.AddonStatus;
|
||||
|
||||
/**
|
||||
* Singleton entry point for LoVisual add-on discovery and lifecycle orchestration. Keeps the
|
||||
* addon registry ({@code ADDONS}), the persisted enabled-overrides and the client-ready flags, and
|
||||
* drives discovery, init and the public lookup/enable API consumed by the ClickGUI and commands.
|
||||
*
|
||||
* <p>Delegates the heavy work within the same package: {@link AddonLifecycle} (per-registration
|
||||
* callbacks), {@link AddonRuntime} (enable/disable + contribution suspend/resume),
|
||||
* {@link AddonDescriptors} (metadata parsing + validation) and {@link AddonProfiles} (config-profile
|
||||
* owner keys), keeping the public static API stable while each file stays under the size caps.</p>
|
||||
*/
|
||||
public enum AddonManager {
|
||||
;
|
||||
|
||||
public static final String ADDON_METADATA_KEY = "lovisual:addon";
|
||||
|
||||
private static final Map<String, AddonRegistration> ADDONS = new LinkedHashMap<>();
|
||||
private static final Map<String, Boolean> ENABLED_OVERRIDES = new LinkedHashMap<>();
|
||||
private static final Map<String, String> MODULE_OWNERS = new LinkedHashMap<>();
|
||||
private static final Map<String, String> DRAGGABLE_HUD_OWNERS = new LinkedHashMap<>();
|
||||
private static final Map<String, String> STATIC_HUD_OWNERS = new LinkedHashMap<>();
|
||||
static final Map<String, AddonRegistration> ADDONS = new LinkedHashMap<>();
|
||||
static final Map<String, Boolean> ENABLED_OVERRIDES = new LinkedHashMap<>();
|
||||
private static boolean discovered;
|
||||
private static boolean moduleLoadingPrepared;
|
||||
private static boolean initialized;
|
||||
private static boolean clientReady;
|
||||
static boolean clientReady;
|
||||
|
||||
/**
|
||||
* Discovers enabled addon entrypoints and lets them configure built-in module
|
||||
|
|
@ -59,7 +42,7 @@ public enum AddonManager {
|
|||
ensureDiscovered();
|
||||
if (moduleLoadingPrepared) return;
|
||||
moduleLoadingPrepared = true;
|
||||
configureEnabledAddons();
|
||||
AddonLifecycle.configureEnabled();
|
||||
}
|
||||
|
||||
public static void init() {
|
||||
|
|
@ -68,9 +51,9 @@ public enum AddonManager {
|
|||
if (!ModuleAutoLoader.isDiscoveryFinished()) {
|
||||
return;
|
||||
}
|
||||
reconcileModuleExclusions();
|
||||
AddonLifecycle.reconcileExclusions();
|
||||
initialized = true;
|
||||
initializeEnabledAddons();
|
||||
AddonLifecycle.initializeEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -81,7 +64,7 @@ public enum AddonManager {
|
|||
if (!initialized) return;
|
||||
clientReady = true;
|
||||
for (AddonRegistration registration : ADDONS.values()) {
|
||||
notifyClientReady(registration);
|
||||
AddonLifecycle.notifyClientReady(registration);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +73,7 @@ public enum AddonManager {
|
|||
discovered = true;
|
||||
ENABLED_OVERRIDES.clear();
|
||||
ENABLED_OVERRIDES.putAll(AddonStateStore.loadEnabledOverrides());
|
||||
discoverEntrypoints();
|
||||
AddonLifecycle.discover();
|
||||
}
|
||||
|
||||
public static List<AddonSnapshot> snapshots() {
|
||||
|
|
@ -117,7 +100,7 @@ public enum AddonManager {
|
|||
init();
|
||||
List<AddonIssue> out = new ArrayList<>();
|
||||
for (AddonRegistration registration : ADDONS.values()) {
|
||||
validateRegistration(registration);
|
||||
AddonDescriptors.validate(registration);
|
||||
out.addAll(registration.issues);
|
||||
}
|
||||
if (out.isEmpty()) {
|
||||
|
|
@ -127,22 +110,19 @@ public enum AddonManager {
|
|||
}
|
||||
|
||||
public static String moduleProfileOwnerId(String moduleId) {
|
||||
String addonId = MODULE_OWNERS.get(normalizeId(moduleId));
|
||||
return addonId == null ? "module:" + moduleId : "addon:" + addonId + ":module:" + moduleId;
|
||||
return AddonProfiles.moduleProfileOwnerId(moduleId);
|
||||
}
|
||||
|
||||
public static String draggableHudProfileOwnerId(String elementId) {
|
||||
String addonId = DRAGGABLE_HUD_OWNERS.get(normalizeId(elementId));
|
||||
return addonId == null ? "hud.draggable:" + elementId : "addon:" + addonId + ":hud.draggable:" + elementId;
|
||||
return AddonProfiles.draggableHudProfileOwnerId(elementId);
|
||||
}
|
||||
|
||||
public static String staticHudProfileOwnerId(String elementId) {
|
||||
String addonId = STATIC_HUD_OWNERS.get(normalizeId(elementId));
|
||||
return addonId == null ? "hud.static:" + elementId : "addon:" + addonId + ":hud.static:" + elementId;
|
||||
return AddonProfiles.staticHudProfileOwnerId(elementId);
|
||||
}
|
||||
|
||||
public static String profileDisplayName(String ownerId, String displayName) {
|
||||
String addonId = addonIdFromProfileOwner(ownerId);
|
||||
String addonId = AddonProfiles.addonIdFromProfileOwner(ownerId);
|
||||
if (addonId == null) return displayName;
|
||||
AddonRegistration registration = ADDONS.get(addonId);
|
||||
String addonName = registration == null ? addonId : registration.descriptor.name();
|
||||
|
|
@ -150,11 +130,7 @@ public enum AddonManager {
|
|||
}
|
||||
|
||||
public static String addonIdFromProfileOwner(String ownerId) {
|
||||
if (ownerId == null || !ownerId.startsWith("addon:")) return null;
|
||||
int start = "addon:".length();
|
||||
int end = ownerId.indexOf(':', start);
|
||||
if (end <= start) return null;
|
||||
return normalizeId(ownerId.substring(start, end));
|
||||
return AddonProfiles.addonIdFromProfileOwner(ownerId);
|
||||
}
|
||||
|
||||
public static boolean setEnabled(String id, boolean enabled) {
|
||||
|
|
@ -170,33 +146,10 @@ public enum AddonManager {
|
|||
AddonStateStore.saveEnabledOverrides(ENABLED_OVERRIDES);
|
||||
|
||||
if (enabled) {
|
||||
if (!registration.moduleConfigurationApplied) {
|
||||
configureAddonModules(registration);
|
||||
}
|
||||
if (!registration.initialized) {
|
||||
initializeAddon(registration);
|
||||
} else {
|
||||
resumeRuntimeContributions(registration);
|
||||
resumeAddon(registration, "enabled by user");
|
||||
if (registration.status != AddonStatus.ERROR) {
|
||||
registration.status = AddonStatus.ACTIVE;
|
||||
}
|
||||
}
|
||||
if (clientReady) {
|
||||
notifyClientReady(registration);
|
||||
}
|
||||
return true;
|
||||
AddonRuntime.enable(registration, clientReady);
|
||||
} else {
|
||||
AddonRuntime.disable(registration);
|
||||
}
|
||||
|
||||
registration.restartRequired = !registration.disabledBuiltInModules.isEmpty();
|
||||
if (!registration.initialized) {
|
||||
registration.status = AddonStatus.DISABLED;
|
||||
return true;
|
||||
}
|
||||
|
||||
suspendRuntimeContributions(registration);
|
||||
suspendAddon(registration, "disabled by user");
|
||||
registration.status = AddonStatus.DISABLED;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -208,279 +161,20 @@ public enum AddonManager {
|
|||
return count;
|
||||
}
|
||||
|
||||
private static void discoverEntrypoints() {
|
||||
for (var entrypoint : FabricLoader.getInstance()
|
||||
.getEntrypointContainers(LoVisualAddon.ENTRYPOINT_KEY, LoVisualAddon.class)) {
|
||||
ModMetadata metadata = entrypoint.getProvider().getMetadata();
|
||||
String id = normalizeId(metadata.getId());
|
||||
AddonDescriptor descriptor = descriptor(metadata, entrypoint.getDefinition());
|
||||
boolean enabled = ENABLED_OVERRIDES.getOrDefault(id, true);
|
||||
AddonStatus status = enabled ? AddonStatus.DISCOVERED : AddonStatus.DISABLED;
|
||||
AddonRegistration duplicate = ADDONS.get(id);
|
||||
if (duplicate != null) {
|
||||
duplicate.issue(AddonIssue.Severity.ERROR, "Duplicate addon id", metadata.getId());
|
||||
continue;
|
||||
}
|
||||
AddonRegistration registration = new AddonRegistration(descriptor, entrypoint, enabled, status);
|
||||
validateRegistration(registration);
|
||||
ADDONS.put(id, registration);
|
||||
}
|
||||
}
|
||||
|
||||
private static void configureEnabledAddons() {
|
||||
for (AddonRegistration registration : ADDONS.values()) {
|
||||
if (registration.enabled) {
|
||||
configureAddonModules(registration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void configureAddonModules(AddonRegistration registration) {
|
||||
if (registration.moduleConfigurationApplied || !registration.enabled || registration.status == AddonStatus.ERROR) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
LoVisualAddon addon = addonInstance(registration);
|
||||
LoVisualModuleLoadContextImpl context = new LoVisualModuleLoadContextImpl(registration);
|
||||
addon.onConfigureModules(context);
|
||||
context.commit();
|
||||
registration.moduleConfigurationApplied = true;
|
||||
} catch (Throwable t) {
|
||||
registration.status = AddonStatus.ERROR;
|
||||
registration.issue(AddonIssue.Severity.ERROR, "Module configuration failed", t.toString());
|
||||
DebugLog.error("Addon module configuration failed: %s", t, registration.descriptor.id());
|
||||
}
|
||||
}
|
||||
|
||||
private static void reconcileModuleExclusions() {
|
||||
for (AddonRegistration registration : ADDONS.values()) {
|
||||
if (registration.disabledBuiltInModules.isEmpty()) continue;
|
||||
registration.disabledBuiltInModules.removeIf(reference -> {
|
||||
if (ModuleAutoLoader.wasDisableRequestMatched(reference, registration.descriptor.id())) {
|
||||
return false;
|
||||
}
|
||||
registration.issue(
|
||||
AddonIssue.Severity.WARNING,
|
||||
"Built-in module exclusion target was not found",
|
||||
reference
|
||||
);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void initializeEnabledAddons() {
|
||||
for (AddonRegistration registration : ADDONS.values()) {
|
||||
if (registration.enabled && registration.status != AddonStatus.ERROR) {
|
||||
initializeAddon(registration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void initializeAddon(AddonRegistration registration) {
|
||||
if (registration.initialized || !registration.enabled || registration.status == AddonStatus.ERROR) return;
|
||||
if (!registration.moduleConfigurationApplied) {
|
||||
configureAddonModules(registration);
|
||||
if (registration.status == AddonStatus.ERROR) return;
|
||||
}
|
||||
try {
|
||||
LoVisualAddon addon = addonInstance(registration);
|
||||
addon.onInitialize(new LoVisualAddonContextImpl(registration));
|
||||
registration.initialized = true;
|
||||
registration.status = AddonStatus.ACTIVE;
|
||||
DebugLog.info("Addon initialized: %s", registration.descriptor.id());
|
||||
if (clientReady) {
|
||||
notifyClientReady(registration);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
registration.status = AddonStatus.ERROR;
|
||||
registration.issue(AddonIssue.Severity.ERROR, "Initialization failed", t.toString());
|
||||
DebugLog.error("Addon initialization failed: %s", t, registration.descriptor.id());
|
||||
}
|
||||
}
|
||||
|
||||
private static void notifyClientReady(AddonRegistration registration) {
|
||||
if (registration == null
|
||||
|| registration.clientReadyNotified
|
||||
|| !registration.initialized
|
||||
|| !registration.enabled
|
||||
|| registration.status != AddonStatus.ACTIVE) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
registration.addon.onClientReady(
|
||||
new LoVisualAddonRuntimeContextImpl(registration.descriptor.id())
|
||||
);
|
||||
registration.clientReadyNotified = true;
|
||||
} catch (Throwable t) {
|
||||
registration.status = AddonStatus.ERROR;
|
||||
registration.issue(AddonIssue.Severity.ERROR, "Client ready callback failed", t.toString());
|
||||
DebugLog.error("Addon client ready callback failed: %s", t, registration.descriptor.id());
|
||||
}
|
||||
}
|
||||
|
||||
private static LoVisualAddon addonInstance(AddonRegistration registration) {
|
||||
LoVisualAddon addon = registration.addon;
|
||||
if (addon == null) {
|
||||
addon = registration.entrypoint.getEntrypoint();
|
||||
registration.addon = addon;
|
||||
}
|
||||
return addon;
|
||||
}
|
||||
|
||||
private static void suspendAddon(AddonRegistration registration, String reason) {
|
||||
if (registration.addon == null) return;
|
||||
try {
|
||||
registration.addon.onRuntimeSuspended(new LoVisualAddonRuntimeContextImpl(registration.descriptor.id()));
|
||||
} catch (Throwable t) {
|
||||
registration.status = AddonStatus.ERROR;
|
||||
registration.issue(AddonIssue.Severity.ERROR, "Runtime suspend failed", t.toString());
|
||||
DebugLog.error("Addon runtime suspend failed: %s", t, registration.descriptor.id());
|
||||
}
|
||||
}
|
||||
|
||||
private static void resumeAddon(AddonRegistration registration, String reason) {
|
||||
if (registration.addon == null) return;
|
||||
try {
|
||||
registration.addon.onRuntimeResumed(new LoVisualAddonRuntimeContextImpl(registration.descriptor.id()));
|
||||
} catch (Throwable t) {
|
||||
registration.status = AddonStatus.ERROR;
|
||||
registration.issue(AddonIssue.Severity.ERROR, "Runtime resume failed", t.toString());
|
||||
DebugLog.error("Addon runtime resume failed: %s", t, registration.descriptor.id());
|
||||
}
|
||||
}
|
||||
|
||||
private static void shutdownAddon(AddonRegistration registration, String reason) {
|
||||
if (registration.addon == null) return;
|
||||
try {
|
||||
registration.addon.onShutdown(new LoVisualAddonRuntimeContextImpl(registration.descriptor.id()));
|
||||
} catch (Throwable t) {
|
||||
registration.issue(AddonIssue.Severity.ERROR, "Shutdown failed", t.toString());
|
||||
DebugLog.error("Addon shutdown failed: %s", t, registration.descriptor.id());
|
||||
}
|
||||
}
|
||||
|
||||
private static void suspendRuntimeContributions(AddonRegistration registration) {
|
||||
registration.suspendedModules.clear();
|
||||
registration.suspendedDraggableHudElements.clear();
|
||||
registration.suspendedStaticHudElements.clear();
|
||||
for (String id : registration.modules) {
|
||||
Module module = ModuleManager.get(id);
|
||||
if (module != null) {
|
||||
registration.suspendedModules.put(id, module.isEnabled());
|
||||
ModuleManager.setRuntimeEnabledTransient(id, false);
|
||||
}
|
||||
}
|
||||
for (String id : registration.draggableHudElements) {
|
||||
DraggableHudElement element = DraggableHudElementRegistry.getById(id);
|
||||
if (element != null) {
|
||||
registration.suspendedDraggableHudElements.put(id, element.isEnabled());
|
||||
element.setRuntimeEnabledTransient(false);
|
||||
}
|
||||
}
|
||||
for (String id : registration.staticHudElements) {
|
||||
AbstractHudElement element = StaticHudElementRegistry.getById(id);
|
||||
if (element != null) {
|
||||
registration.suspendedStaticHudElements.put(id, element.isEnabled());
|
||||
element.setRuntimeEnabledTransient(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void resumeRuntimeContributions(AddonRegistration registration) {
|
||||
for (String id : registration.modules) {
|
||||
Boolean enabled = registration.suspendedModules.get(id);
|
||||
if (enabled != null) {
|
||||
ModuleManager.setRuntimeEnabledTransient(id, enabled);
|
||||
}
|
||||
}
|
||||
for (String id : registration.draggableHudElements) {
|
||||
Boolean enabled = registration.suspendedDraggableHudElements.get(id);
|
||||
DraggableHudElement element = DraggableHudElementRegistry.getById(id);
|
||||
if (enabled != null && element != null) {
|
||||
element.setRuntimeEnabledTransient(enabled);
|
||||
}
|
||||
}
|
||||
for (String id : registration.staticHudElements) {
|
||||
Boolean enabled = registration.suspendedStaticHudElements.get(id);
|
||||
AbstractHudElement element = StaticHudElementRegistry.getById(id);
|
||||
if (enabled != null && element != null) {
|
||||
element.setRuntimeEnabledTransient(enabled);
|
||||
}
|
||||
}
|
||||
registration.suspendedModules.clear();
|
||||
registration.suspendedDraggableHudElements.clear();
|
||||
registration.suspendedStaticHudElements.clear();
|
||||
}
|
||||
|
||||
public static void noteModuleOwner(String addonId, String moduleId) {
|
||||
|
||||
if (moduleId != null && !moduleId.isBlank()) {
|
||||
MODULE_OWNERS.put(normalizeId(moduleId), normalizeId(addonId));
|
||||
}
|
||||
AddonProfiles.noteModuleOwner(addonId, moduleId);
|
||||
}
|
||||
|
||||
public static void noteDraggableHudOwner(String addonId, String elementId) {
|
||||
|
||||
if (elementId != null && !elementId.isBlank()) {
|
||||
DRAGGABLE_HUD_OWNERS.put(normalizeId(elementId), normalizeId(addonId));
|
||||
}
|
||||
AddonProfiles.noteDraggableHudOwner(addonId, elementId);
|
||||
}
|
||||
|
||||
public static void noteStaticHudOwner(String addonId, String elementId) {
|
||||
|
||||
if (elementId != null && !elementId.isBlank()) {
|
||||
STATIC_HUD_OWNERS.put(normalizeId(elementId), normalizeId(addonId));
|
||||
}
|
||||
AddonProfiles.noteStaticHudOwner(addonId, elementId);
|
||||
}
|
||||
|
||||
private static void validateRegistration(AddonRegistration registration) {
|
||||
if (registration.descriptor.apiVersion() != LoVisualAddon.API_VERSION) {
|
||||
registration.issue(
|
||||
AddonIssue.Severity.WARNING,
|
||||
"Addon API version differs from client API",
|
||||
"addon=" + registration.descriptor.apiVersion() + ", client=" + LoVisualAddon.API_VERSION
|
||||
);
|
||||
}
|
||||
if (registration.descriptor.iconPath() == null || registration.descriptor.iconPath().isBlank()) {
|
||||
registration.issue(AddonIssue.Severity.WARNING, "Addon icon is missing", "Add icon in fabric.mod.json");
|
||||
}
|
||||
if (registration.descriptor.entrypointDefinition() == null
|
||||
|| registration.descriptor.entrypointDefinition().isBlank()) {
|
||||
registration.issue(AddonIssue.Severity.WARNING, "Entrypoint definition is unavailable", "");
|
||||
}
|
||||
}
|
||||
|
||||
private static AddonDescriptor descriptor(ModMetadata metadata, String entrypointDefinition) {
|
||||
List<String> authors = metadata.getAuthors().stream()
|
||||
.map(person -> person.getName() == null ? "" : person.getName())
|
||||
.filter(name -> !name.isBlank())
|
||||
.toList();
|
||||
return new AddonDescriptor(
|
||||
normalizeId(metadata.getId()),
|
||||
blankFallback(metadata.getName(), metadata.getId()),
|
||||
String.valueOf(metadata.getVersion()),
|
||||
metadata.getDescription() == null ? "" : metadata.getDescription(),
|
||||
authors,
|
||||
metadata.getIconPath(64).or(() -> metadata.getIconPath(32)).orElse(""),
|
||||
apiVersion(metadata),
|
||||
entrypointDefinition
|
||||
);
|
||||
}
|
||||
|
||||
private static int apiVersion(ModMetadata metadata) {
|
||||
CustomValue value = metadata.getCustomValue(ADDON_METADATA_KEY);
|
||||
if (value == null || value.getType() != CustomValue.CvType.OBJECT) return LoVisualAddon.API_VERSION;
|
||||
CustomValue api = value.getAsObject().get("api");
|
||||
if (api == null || api.getType() != CustomValue.CvType.NUMBER) return LoVisualAddon.API_VERSION;
|
||||
return api.getAsNumber().intValue();
|
||||
}
|
||||
|
||||
private static String blankFallback(String value, String fallback) {
|
||||
return value == null || value.isBlank() ? fallback : value;
|
||||
}
|
||||
|
||||
private static String normalizeId(String id) {
|
||||
/** Locale-independent lowercase key used for every addon / module / HUD id lookup. */
|
||||
static String normalizeId(String id) {
|
||||
return id == null ? "" : id.trim().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.addon.core;
|
||||
|
||||
import dev.loki.lovisual.api.v0.addon.LoVisualAddon;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
package dev.loki.lovisual.addon.core;
|
||||
|
||||
import dev.loki.lovisual.api.v0.addon.LoVisualAddon;
|
||||
import net.fabricmc.loader.api.metadata.CustomValue;
|
||||
import net.fabricmc.loader.api.metadata.ModMetadata;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Builds an {@link AddonDescriptor} from Fabric mod metadata and validates a registration's
|
||||
* sanity (API-version match, icon and entrypoint presence), recording findings as
|
||||
* {@link AddonIssue}s on the registration. Pure helpers over data the caller already holds,
|
||||
* extracted from {@link AddonManager} to keep it under the file-size cap. Shares
|
||||
* {@code addon.core}'s package so {@link AddonManager#normalizeId} stays reachable.
|
||||
*/
|
||||
final class AddonDescriptors {
|
||||
|
||||
private AddonDescriptors() { }
|
||||
|
||||
/** Parse a descriptor from the mod's metadata plus its resolved entrypoint definition. */
|
||||
static AddonDescriptor descriptor(ModMetadata metadata, String entrypointDefinition) {
|
||||
List<String> authors = metadata.getAuthors().stream()
|
||||
.map(person -> person.getName() == null ? "" : person.getName())
|
||||
.filter(name -> !name.isBlank())
|
||||
.toList();
|
||||
return new AddonDescriptor(
|
||||
AddonManager.normalizeId(metadata.getId()),
|
||||
blankFallback(metadata.getName(), metadata.getId()),
|
||||
String.valueOf(metadata.getVersion()),
|
||||
metadata.getDescription() == null ? "" : metadata.getDescription(),
|
||||
authors,
|
||||
metadata.getIconPath(64).or(() -> metadata.getIconPath(32)).orElse(""),
|
||||
apiVersion(metadata),
|
||||
entrypointDefinition
|
||||
);
|
||||
}
|
||||
|
||||
/** Read the declared LoVisual API version from the {@code lovisual:addon} custom block. */
|
||||
static int apiVersion(ModMetadata metadata) {
|
||||
CustomValue value = metadata.getCustomValue(AddonManager.ADDON_METADATA_KEY);
|
||||
if (value == null || value.getType() != CustomValue.CvType.OBJECT) return LoVisualAddon.API_VERSION;
|
||||
CustomValue api = value.getAsObject().get("api");
|
||||
if (api == null || api.getType() != CustomValue.CvType.NUMBER) return LoVisualAddon.API_VERSION;
|
||||
return api.getAsNumber().intValue();
|
||||
}
|
||||
|
||||
/** Flag API-version drift, a missing icon and an unavailable entrypoint definition. */
|
||||
static void validate(AddonRegistration registration) {
|
||||
if (registration.descriptor.apiVersion() != LoVisualAddon.API_VERSION) {
|
||||
registration.issue(
|
||||
AddonIssue.Severity.WARNING,
|
||||
"Addon API version differs from client API",
|
||||
"addon=" + registration.descriptor.apiVersion() + ", client=" + LoVisualAddon.API_VERSION
|
||||
);
|
||||
}
|
||||
if (registration.descriptor.iconPath() == null || registration.descriptor.iconPath().isBlank()) {
|
||||
registration.issue(AddonIssue.Severity.WARNING, "Addon icon is missing", "Add icon in fabric.mod.json");
|
||||
}
|
||||
if (registration.descriptor.entrypointDefinition() == null
|
||||
|| registration.descriptor.entrypointDefinition().isBlank()) {
|
||||
registration.issue(AddonIssue.Severity.WARNING, "Entrypoint definition is unavailable", "");
|
||||
}
|
||||
}
|
||||
|
||||
private static String blankFallback(String value, String fallback) {
|
||||
return value == null || value.isBlank() ? fallback : value;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
package dev.loki.lovisual.addon.core;
|
||||
|
||||
import dev.loki.lovisual.addon.context.LoVisualAddonContextImpl;
|
||||
import dev.loki.lovisual.addon.context.LoVisualAddonRuntimeContextImpl;
|
||||
import dev.loki.lovisual.addon.context.LoVisualModuleLoadContextImpl;
|
||||
import dev.loki.lovisual.addon.pipeline.AddonStatus;
|
||||
import dev.loki.lovisual.api.v0.addon.LoVisualAddon;
|
||||
import dev.loki.lovisual.features.module.lifecycle.ModuleAutoLoader;
|
||||
import dev.loki.lovisual.util.logging.DebugLog;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.fabricmc.loader.api.metadata.ModMetadata;
|
||||
|
||||
/**
|
||||
* Per-registration lifecycle callbacks backing {@link AddonManager}: lazy entrypoint
|
||||
* materialization plus the {@code onConfigureModules} / {@code onInitialize} /
|
||||
* {@code onClientReady} / {@code onRuntimeSuspended} / {@code onRuntimeResumed} /
|
||||
* {@code onShutdown} invocations, each guarded by the registration's flags and turning any
|
||||
* thrown error into an {@link AddonIssue} + {@link AddonStatus#ERROR}. Shares {@code addon.core}'s
|
||||
* package so the registration's package-private state stays reachable; lives in a subfolder to
|
||||
* keep {@link AddonManager} under the file-size cap.
|
||||
*/
|
||||
final class AddonLifecycle {
|
||||
|
||||
private AddonLifecycle() { }
|
||||
|
||||
/** Discover addon entrypoints from the Fabric loader and register each as a new addon. */
|
||||
static void discover() {
|
||||
for (var entrypoint : FabricLoader.getInstance()
|
||||
.getEntrypointContainers(LoVisualAddon.ENTRYPOINT_KEY, LoVisualAddon.class)) {
|
||||
ModMetadata metadata = entrypoint.getProvider().getMetadata();
|
||||
String id = AddonManager.normalizeId(metadata.getId());
|
||||
AddonDescriptor descriptor = AddonDescriptors.descriptor(metadata, entrypoint.getDefinition());
|
||||
boolean enabled = AddonManager.ENABLED_OVERRIDES.getOrDefault(id, true);
|
||||
AddonStatus status = enabled ? AddonStatus.DISCOVERED : AddonStatus.DISABLED;
|
||||
AddonRegistration duplicate = AddonManager.ADDONS.get(id);
|
||||
if (duplicate != null) {
|
||||
duplicate.issue(AddonIssue.Severity.ERROR, "Duplicate addon id", metadata.getId());
|
||||
continue;
|
||||
}
|
||||
AddonRegistration registration = new AddonRegistration(descriptor, entrypoint, enabled, status);
|
||||
AddonDescriptors.validate(registration);
|
||||
AddonManager.ADDONS.put(id, registration);
|
||||
}
|
||||
}
|
||||
|
||||
/** Configure modules for every currently-enabled addon (pre-instantiation pass). */
|
||||
static void configureEnabled() {
|
||||
for (AddonRegistration registration : AddonManager.ADDONS.values()) {
|
||||
if (registration.enabled) {
|
||||
configure(registration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Drop built-in module exclusions whose target was never found, warning about each. */
|
||||
static void reconcileExclusions() {
|
||||
for (AddonRegistration registration : AddonManager.ADDONS.values()) {
|
||||
if (registration.disabledBuiltInModules.isEmpty()) continue;
|
||||
registration.disabledBuiltInModules.removeIf(reference -> {
|
||||
if (ModuleAutoLoader.wasDisableRequestMatched(reference, registration.descriptor.id())) {
|
||||
return false;
|
||||
}
|
||||
registration.issue(
|
||||
AddonIssue.Severity.WARNING,
|
||||
"Built-in module exclusion target was not found",
|
||||
reference
|
||||
);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Initialize every enabled, non-errored addon. */
|
||||
static void initializeEnabled() {
|
||||
for (AddonRegistration registration : AddonManager.ADDONS.values()) {
|
||||
if (registration.enabled && registration.status != AddonStatus.ERROR) {
|
||||
initialize(registration, AddonManager.clientReady);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Let the addon configure built-in module exclusions once, before modules are instantiated. */
|
||||
static void configure(AddonRegistration registration) {
|
||||
if (registration.moduleConfigurationApplied || !registration.enabled || registration.status == AddonStatus.ERROR) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
LoVisualAddon addon = addonInstance(registration);
|
||||
LoVisualModuleLoadContextImpl context = new LoVisualModuleLoadContextImpl(registration);
|
||||
addon.onConfigureModules(context);
|
||||
context.commit();
|
||||
registration.moduleConfigurationApplied = true;
|
||||
} catch (Throwable t) {
|
||||
registration.status = AddonStatus.ERROR;
|
||||
registration.issue(AddonIssue.Severity.ERROR, "Module configuration failed", t.toString());
|
||||
DebugLog.error("Addon module configuration failed: %s", t, registration.descriptor.id());
|
||||
}
|
||||
}
|
||||
|
||||
/** Initialize the addon (configuring modules first if needed); notifies client-ready if up. */
|
||||
static void initialize(AddonRegistration registration, boolean clientReady) {
|
||||
if (registration.initialized || !registration.enabled || registration.status == AddonStatus.ERROR) return;
|
||||
if (!registration.moduleConfigurationApplied) {
|
||||
configure(registration);
|
||||
if (registration.status == AddonStatus.ERROR) return;
|
||||
}
|
||||
try {
|
||||
LoVisualAddon addon = addonInstance(registration);
|
||||
addon.onInitialize(new LoVisualAddonContextImpl(registration));
|
||||
registration.initialized = true;
|
||||
registration.status = AddonStatus.ACTIVE;
|
||||
DebugLog.info("Addon initialized: %s", registration.descriptor.id());
|
||||
if (clientReady) {
|
||||
notifyClientReady(registration);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
registration.status = AddonStatus.ERROR;
|
||||
registration.issue(AddonIssue.Severity.ERROR, "Initialization failed", t.toString());
|
||||
DebugLog.error("Addon initialization failed: %s", t, registration.descriptor.id());
|
||||
}
|
||||
}
|
||||
|
||||
/** Fire {@code onClientReady} once, after module configs have been applied. */
|
||||
static void notifyClientReady(AddonRegistration registration) {
|
||||
if (registration == null
|
||||
|| registration.clientReadyNotified
|
||||
|| !registration.initialized
|
||||
|| !registration.enabled
|
||||
|| registration.status != AddonStatus.ACTIVE) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
registration.addon.onClientReady(
|
||||
new LoVisualAddonRuntimeContextImpl(registration.descriptor.id())
|
||||
);
|
||||
registration.clientReadyNotified = true;
|
||||
} catch (Throwable t) {
|
||||
registration.status = AddonStatus.ERROR;
|
||||
registration.issue(AddonIssue.Severity.ERROR, "Client ready callback failed", t.toString());
|
||||
DebugLog.error("Addon client ready callback failed: %s", t, registration.descriptor.id());
|
||||
}
|
||||
}
|
||||
|
||||
/** Fire {@code onRuntimeSuspended} for an addon being toggled off at runtime. */
|
||||
static void suspend(AddonRegistration registration, String reason) {
|
||||
if (registration.addon == null) return;
|
||||
try {
|
||||
registration.addon.onRuntimeSuspended(new LoVisualAddonRuntimeContextImpl(registration.descriptor.id()));
|
||||
} catch (Throwable t) {
|
||||
registration.status = AddonStatus.ERROR;
|
||||
registration.issue(AddonIssue.Severity.ERROR, "Runtime suspend failed", t.toString());
|
||||
DebugLog.error("Addon runtime suspend failed: %s", t, registration.descriptor.id());
|
||||
}
|
||||
}
|
||||
|
||||
/** Fire {@code onRuntimeResumed} for an addon being toggled back on at runtime. */
|
||||
static void resume(AddonRegistration registration, String reason) {
|
||||
if (registration.addon == null) return;
|
||||
try {
|
||||
registration.addon.onRuntimeResumed(new LoVisualAddonRuntimeContextImpl(registration.descriptor.id()));
|
||||
} catch (Throwable t) {
|
||||
registration.status = AddonStatus.ERROR;
|
||||
registration.issue(AddonIssue.Severity.ERROR, "Runtime resume failed", t.toString());
|
||||
DebugLog.error("Addon runtime resume failed: %s", t, registration.descriptor.id());
|
||||
}
|
||||
}
|
||||
|
||||
/** Fire {@code onShutdown} on full addon teardown. */
|
||||
static void shutdown(AddonRegistration registration, String reason) {
|
||||
if (registration.addon == null) return;
|
||||
try {
|
||||
registration.addon.onShutdown(new LoVisualAddonRuntimeContextImpl(registration.descriptor.id()));
|
||||
} catch (Throwable t) {
|
||||
registration.issue(AddonIssue.Severity.ERROR, "Shutdown failed", t.toString());
|
||||
DebugLog.error("Addon shutdown failed: %s", t, registration.descriptor.id());
|
||||
}
|
||||
}
|
||||
|
||||
/** Materialize (and memoize) the addon instance from its entrypoint container. */
|
||||
static LoVisualAddon addonInstance(AddonRegistration registration) {
|
||||
LoVisualAddon addon = registration.addon;
|
||||
if (addon == null) {
|
||||
addon = registration.entrypoint.getEntrypoint();
|
||||
registration.addon = addon;
|
||||
}
|
||||
return addon;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package dev.loki.lovisual.addon.core;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Owns the id → addon-id reverse indexes for contributed modules, draggable-HUD and static-HUD
|
||||
* elements, and turns them into the namespaced config-profile owner keys the profile system uses
|
||||
* ({@code addon:<addon>:module:<id>} when an addon owns the entry, plain otherwise). Extracted
|
||||
* from {@link AddonManager} to keep it under the file-size cap; shares {@code addon.core}'s
|
||||
* package so {@link AddonManager#normalizeId} stays reachable.
|
||||
*/
|
||||
final class AddonProfiles {
|
||||
|
||||
private static final Map<String, String> MODULE_OWNERS = new LinkedHashMap<>();
|
||||
private static final Map<String, String> DRAGGABLE_HUD_OWNERS = new LinkedHashMap<>();
|
||||
private static final Map<String, String> STATIC_HUD_OWNERS = new LinkedHashMap<>();
|
||||
|
||||
private AddonProfiles() { }
|
||||
|
||||
/** Record that {@code addonId} owns the given module / draggable-HUD / static-HUD element. */
|
||||
static void noteModuleOwner(String addonId, String moduleId) {
|
||||
if (moduleId != null && !moduleId.isBlank()) {
|
||||
MODULE_OWNERS.put(AddonManager.normalizeId(moduleId), AddonManager.normalizeId(addonId));
|
||||
}
|
||||
}
|
||||
|
||||
static void noteDraggableHudOwner(String addonId, String elementId) {
|
||||
if (elementId != null && !elementId.isBlank()) {
|
||||
DRAGGABLE_HUD_OWNERS.put(AddonManager.normalizeId(elementId), AddonManager.normalizeId(addonId));
|
||||
}
|
||||
}
|
||||
|
||||
static void noteStaticHudOwner(String addonId, String elementId) {
|
||||
if (elementId != null && !elementId.isBlank()) {
|
||||
STATIC_HUD_OWNERS.put(AddonManager.normalizeId(elementId), AddonManager.normalizeId(addonId));
|
||||
}
|
||||
}
|
||||
|
||||
/** Namespaced config-profile owner key for a module. */
|
||||
static String moduleProfileOwnerId(String moduleId) {
|
||||
String addonId = MODULE_OWNERS.get(AddonManager.normalizeId(moduleId));
|
||||
return addonId == null ? "module:" + moduleId : "addon:" + addonId + ":module:" + moduleId;
|
||||
}
|
||||
|
||||
/** Namespaced config-profile owner key for a draggable HUD element. */
|
||||
static String draggableHudProfileOwnerId(String elementId) {
|
||||
String addonId = DRAGGABLE_HUD_OWNERS.get(AddonManager.normalizeId(elementId));
|
||||
return addonId == null ? "hud.draggable:" + elementId : "addon:" + addonId + ":hud.draggable:" + elementId;
|
||||
}
|
||||
|
||||
/** Namespaced config-profile owner key for a static HUD element. */
|
||||
static String staticHudProfileOwnerId(String elementId) {
|
||||
String addonId = STATIC_HUD_OWNERS.get(AddonManager.normalizeId(elementId));
|
||||
return addonId == null ? "hud.static:" + elementId : "addon:" + addonId + ":hud.static:" + elementId;
|
||||
}
|
||||
|
||||
/** Extract the owning addon id from an {@code addon:<id>:...} owner key, or {@code null}. */
|
||||
static String addonIdFromProfileOwner(String ownerId) {
|
||||
if (ownerId == null || !ownerId.startsWith("addon:")) return null;
|
||||
int start = "addon:".length();
|
||||
int end = ownerId.indexOf(':', start);
|
||||
if (end <= start) return null;
|
||||
return AddonManager.normalizeId(ownerId.substring(start, end));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
package dev.loki.lovisual.addon.core;
|
||||
|
||||
import dev.loki.lovisual.addon.pipeline.AddonStatus;
|
||||
import dev.loki.lovisual.features.gui.hud.base.AbstractHudElement;
|
||||
import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement;
|
||||
import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry;
|
||||
import dev.loki.lovisual.features.gui.hud.nondraggable.StaticHudElementRegistry;
|
||||
import dev.loki.lovisual.features.module.core.Module;
|
||||
import dev.loki.lovisual.features.module.core.ModuleManager;
|
||||
|
||||
/**
|
||||
* Runtime enable/disable transitions for a single addon, backing {@link AddonManager#setEnabled}.
|
||||
* Applies the contributed modules / draggable-HUD / static-HUD elements transiently (recording
|
||||
* their prior enabled state so it can be restored) and drives the {@code onRuntimeResumed} /
|
||||
* {@code onRuntimeSuspended} callbacks via {@link AddonLifecycle}. Shares {@code addon.core}'s
|
||||
* package for package-private registration access; lives in a subfolder to keep the manager small.
|
||||
*/
|
||||
final class AddonRuntime {
|
||||
|
||||
private AddonRuntime() { }
|
||||
|
||||
/** Bring a just-enabled addon up: configure, initialize-or-resume, and notify if client-ready. */
|
||||
static void enable(AddonRegistration registration, boolean clientReady) {
|
||||
if (!registration.moduleConfigurationApplied) {
|
||||
AddonLifecycle.configure(registration);
|
||||
}
|
||||
if (!registration.initialized) {
|
||||
AddonLifecycle.initialize(registration, clientReady);
|
||||
} else {
|
||||
resumeContributions(registration);
|
||||
AddonLifecycle.resume(registration, "enabled by user");
|
||||
if (registration.status != AddonStatus.ERROR) {
|
||||
registration.status = AddonStatus.ACTIVE;
|
||||
}
|
||||
}
|
||||
if (clientReady) {
|
||||
AddonLifecycle.notifyClientReady(registration);
|
||||
}
|
||||
}
|
||||
|
||||
/** Take a just-disabled addon down: flag a restart if it excluded built-ins, then suspend. */
|
||||
static void disable(AddonRegistration registration) {
|
||||
registration.restartRequired = !registration.disabledBuiltInModules.isEmpty();
|
||||
if (!registration.initialized) {
|
||||
registration.status = AddonStatus.DISABLED;
|
||||
return;
|
||||
}
|
||||
suspendContributions(registration);
|
||||
AddonLifecycle.suspend(registration, "disabled by user");
|
||||
registration.status = AddonStatus.DISABLED;
|
||||
}
|
||||
|
||||
/** Record and transiently disable every module / HUD element the addon contributes. */
|
||||
static void suspendContributions(AddonRegistration registration) {
|
||||
registration.suspendedModules.clear();
|
||||
registration.suspendedDraggableHudElements.clear();
|
||||
registration.suspendedStaticHudElements.clear();
|
||||
for (String id : registration.modules) {
|
||||
Module module = ModuleManager.get(id);
|
||||
if (module != null) {
|
||||
registration.suspendedModules.put(id, module.isEnabled());
|
||||
ModuleManager.setRuntimeEnabledTransient(id, false);
|
||||
}
|
||||
}
|
||||
for (String id : registration.draggableHudElements) {
|
||||
DraggableHudElement element = DraggableHudElementRegistry.getById(id);
|
||||
if (element != null) {
|
||||
registration.suspendedDraggableHudElements.put(id, element.isEnabled());
|
||||
element.setRuntimeEnabledTransient(false);
|
||||
}
|
||||
}
|
||||
for (String id : registration.staticHudElements) {
|
||||
AbstractHudElement element = StaticHudElementRegistry.getById(id);
|
||||
if (element != null) {
|
||||
registration.suspendedStaticHudElements.put(id, element.isEnabled());
|
||||
element.setRuntimeEnabledTransient(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Restore the pre-suspend enabled state of every contributed module / HUD element. */
|
||||
static void resumeContributions(AddonRegistration registration) {
|
||||
for (String id : registration.modules) {
|
||||
Boolean enabled = registration.suspendedModules.get(id);
|
||||
if (enabled != null) {
|
||||
ModuleManager.setRuntimeEnabledTransient(id, enabled);
|
||||
}
|
||||
}
|
||||
for (String id : registration.draggableHudElements) {
|
||||
Boolean enabled = registration.suspendedDraggableHudElements.get(id);
|
||||
DraggableHudElement element = DraggableHudElementRegistry.getById(id);
|
||||
if (enabled != null && element != null) {
|
||||
element.setRuntimeEnabledTransient(enabled);
|
||||
}
|
||||
}
|
||||
for (String id : registration.staticHudElements) {
|
||||
Boolean enabled = registration.suspendedStaticHudElements.get(id);
|
||||
AbstractHudElement element = StaticHudElementRegistry.getById(id);
|
||||
if (enabled != null && element != null) {
|
||||
element.setRuntimeEnabledTransient(enabled);
|
||||
}
|
||||
}
|
||||
registration.suspendedModules.clear();
|
||||
registration.suspendedDraggableHudElements.clear();
|
||||
registration.suspendedStaticHudElements.clear();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.addon.manager;
|
||||
|
||||
import dev.loki.lovisual.api.v0.clickgui.LoVisualClickGuiSection;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.addon.manager;
|
||||
|
||||
import dev.loki.lovisual.api.v0.module.LoVisualModuleExtension;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.addon.pipeline;
|
||||
|
||||
import com.mojang.blaze3d.textures.GpuTextureView;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.addon.pipeline;
|
||||
|
||||
import java.util.List;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.addon.pipeline;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.addon.pipeline;
|
||||
|
||||
public enum AddonStatus {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.api.v0.addon;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.api.v0.addon;
|
||||
|
||||
import dev.loki.lovisual.events.meta.UsedImplicitly;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.api.v0.addon;
|
||||
|
||||
import dev.loki.lovisual.api.v0.client.LoVisualClientApi;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.api.v0.addon;
|
||||
|
||||
import dev.loki.lovisual.events.meta.UsedImplicitly;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.api.v0.clickgui;
|
||||
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.api.v0.clickgui;
|
||||
|
||||
public interface LoVisualClickGuiSection {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.api.v0.client;
|
||||
|
||||
import dev.loki.lovisual.features.module.core.Module;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.api.v0.module;
|
||||
|
||||
import dev.loki.lovisual.features.module.core.Module;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.api.v0.module;
|
||||
|
||||
import dev.loki.lovisual.config.definition.SettingDef;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.api.v0.render;
|
||||
|
||||
import com.mojang.blaze3d.PrimitiveTopology;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.api.v0.render;
|
||||
|
||||
public enum LoVisualRenderStage {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.api.v0.render;
|
||||
|
||||
import com.mojang.blaze3d.buffers.GpuBufferSlice;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.api.v0.render.callback;
|
||||
|
||||
@FunctionalInterface
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.api.v0.render.callback;
|
||||
|
||||
import com.mojang.blaze3d.textures.GpuTextureView;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.api.v0.render.callback;
|
||||
|
||||
@FunctionalInterface
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.api.v0.render.callback;
|
||||
import dev.loki.lovisual.api.v0.render.LoVisualRenderStage;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.compat.xaero;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.compat.xaero;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.compat.xaero;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.compat.xaero.triangulator;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.compat.xaero.triangulator;
|
||||
|
||||
import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.compat.xaero.triangulator;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.common;
|
||||
|
||||
import java.util.Map;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.common;
|
||||
|
||||
import java.util.List;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.common;
|
||||
|
||||
import dev.loki.lovisual.config.common.impl.common.CombatPriority;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.common.impl.common;
|
||||
|
||||
import dev.loki.lovisual.config.common.CommonSettingSchema;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.common.impl.common;
|
||||
|
||||
import dev.loki.lovisual.config.common.CommonBooleanGroupSchema;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.common.impl.common;
|
||||
|
||||
import dev.loki.lovisual.config.common.CommonSettingSchema;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.common.impl.common;
|
||||
|
||||
import dev.loki.lovisual.config.common.CommonBooleanGroupSchema;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.common.impl.rotation;
|
||||
|
||||
import dev.loki.lovisual.config.common.CommonSettingSchema;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.common.impl.rotation;
|
||||
|
||||
import dev.loki.lovisual.config.common.CommonSettingSchema;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.common.impl.rotation;
|
||||
|
||||
import dev.loki.lovisual.config.common.CommonSettingSchema;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.common.impl.rotation;
|
||||
|
||||
import dev.loki.lovisual.config.common.CommonSettingSchema;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.common.impl.wall;
|
||||
|
||||
import dev.loki.lovisual.config.common.CommonSettingSchema;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.common.impl.wall;
|
||||
|
||||
import dev.loki.lovisual.config.common.CommonSettingSchema;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.common.impl.wall;
|
||||
|
||||
import dev.loki.lovisual.config.common.CommonBooleanGroupSchema;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.definition;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.definition;
|
||||
|
||||
import dev.loki.lovisual.config.values.collection.ConfigValue;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.definition;
|
||||
|
||||
import dev.loki.lovisual.config.common.CommonSettingSchema;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.definition;
|
||||
import dev.loki.lovisual.features.module.core.Module;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.i18n;
|
||||
|
||||
import net.minecraft.client.resources.language.I18n;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.i18n;
|
||||
|
||||
import java.util.List;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.i18n;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.i18n;
|
||||
|
||||
import dev.loki.lovisual.config.values.collection.ConfigValue;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.io;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
|
|
|||
|
|
@ -1,108 +1,36 @@
|
|||
/*
|
||||
* 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.config.io;
|
||||
|
||||
import com.google.gson.*;
|
||||
import de.marhali.json5.*;
|
||||
import dev.loki.lovisual.config.definition.ConfigObject;
|
||||
import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig;
|
||||
import dev.loki.lovisual.features.module.core.Module;
|
||||
import dev.loki.lovisual.util.logging.DebugLog;
|
||||
import io.github.classgraph.ClassGraph;
|
||||
import io.github.classgraph.ClassInfo;
|
||||
import io.github.classgraph.ScanResult;
|
||||
import dev.loki.lovisual.config.values.collection.ConfigValue;
|
||||
import dev.loki.lovisual.features.gui.hud.base.AbstractHudElement;
|
||||
import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig;
|
||||
import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement;
|
||||
import dev.loki.lovisual.features.module.core.Module;
|
||||
import dev.loki.lovisual.features.relations.EntityFilters;
|
||||
import dev.loki.lovisual.util.logging.DebugLog;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import dev.loki.lovisual.config.definition.ConfigNameProvider;
|
||||
import dev.loki.lovisual.config.definition.ConfigObject;
|
||||
import dev.loki.lovisual.config.i18n.ConfigValueAliasProvider;
|
||||
import dev.loki.lovisual.config.io.ConfigPaths;
|
||||
import dev.loki.lovisual.config.io.JsonConfigObject;
|
||||
|
||||
public enum ConfigSerializer {
|
||||
;
|
||||
|
||||
private static final Path CONFIG_DIR = ConfigPaths.root();
|
||||
private static final long SAVE_DEBOUNCE_MS = 220L;
|
||||
private static final ScheduledExecutorService SAVE_EXECUTOR =
|
||||
Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "LoVisual-ConfigSaver");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
private static final Object SAVE_LOCK = new Object();
|
||||
private static final Map<Path, String> PENDING_TEXT = new HashMap<>();
|
||||
private static final Map<Path, ScheduledFuture<?>> PENDING_TASKS = new HashMap<>();
|
||||
private static final Map<Path, Long> PENDING_TOKENS = new HashMap<>();
|
||||
// One Json5 instance for utilities (player relations, filters, debug config, etc.)
|
||||
private static final Json5 JSON5 = Json5.builder(builder -> builder
|
||||
.trailingComma() // allow trailing commas
|
||||
.indentFactor(2) // pretty output
|
||||
.quoteSingle() // use " quotes
|
||||
.build()
|
||||
);
|
||||
// Gson serializer for modules (strict JSON)
|
||||
private static final Gson GSON = new GsonBuilder()
|
||||
.setPrettyPrinting()
|
||||
.disableHtmlEscaping()
|
||||
.create();
|
||||
private static long SAVE_TOKEN_SEQ = 0L;
|
||||
|
||||
// =============================================================
|
||||
// Bulk load / save helpers
|
||||
// =============================================================
|
||||
|
||||
@SuppressWarnings("unused") // kept for bulk-loading singletons via ClassGraph
|
||||
public static void loadAll() {
|
||||
try (ScanResult sc = new ClassGraph()
|
||||
.enableClassInfo()
|
||||
.acceptPackages("dev.loki.lovisual")
|
||||
.scan()) {
|
||||
|
||||
try (ScanResult sc = new ClassGraph().enableClassInfo().acceptPackages("dev.loki.lovisual").scan()) {
|
||||
for (ClassInfo ci : sc.getClassesImplementing(ConfigObject.class)) {
|
||||
Class<?> cls = ci.loadClass();
|
||||
ConfigObject inst = singletonConfigObjectFor(cls);
|
||||
if (inst != null) {
|
||||
load(inst);
|
||||
}
|
||||
ConfigObject inst = singletonConfigObjectFor(ci.loadClass());
|
||||
if (inst != null) load(inst);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
DebugLog.error("ConfigSerializer.loadAll failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void saveAll() {
|
||||
try (ScanResult sc = new ClassGraph()
|
||||
.enableClassInfo()
|
||||
.acceptPackages("dev.loki.lovisual")
|
||||
.scan()) {
|
||||
|
||||
try (ScanResult sc = new ClassGraph().enableClassInfo().acceptPackages("dev.loki.lovisual").scan()) {
|
||||
for (ClassInfo ci : sc.getClassesImplementing(ConfigObject.class)) {
|
||||
Class<?> cls = ci.loadClass();
|
||||
ConfigObject inst = singletonConfigObjectFor(cls);
|
||||
if (inst != null) {
|
||||
save(inst);
|
||||
}
|
||||
ConfigObject inst = singletonConfigObjectFor(ci.loadClass());
|
||||
if (inst != null) save(inst);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
DebugLog.error("ConfigSerializer.saveAll failed", e);
|
||||
}
|
||||
|
|
@ -110,559 +38,47 @@ public enum ConfigSerializer {
|
|||
|
||||
private static ConfigObject singletonConfigObjectFor(Class<?> cls) throws IllegalAccessException {
|
||||
if (cls == null) return null;
|
||||
|
||||
for (Field field : cls.getDeclaredFields()) {
|
||||
if (!Modifier.isStatic(field.getModifiers())) continue;
|
||||
if (!ConfigObject.class.isAssignableFrom(field.getType())) continue;
|
||||
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(null);
|
||||
if (value instanceof ConfigObject configObject) {
|
||||
return configObject;
|
||||
}
|
||||
if (value instanceof ConfigObject co) return co;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// =============================================================
|
||||
// Dispatch
|
||||
// =============================================================
|
||||
|
||||
public static void save(ConfigObject obj) {
|
||||
if (obj instanceof HudGlobalConfig) return;
|
||||
saveNow(obj);
|
||||
ConfigSaveScheduler.saveNow(buildWrite(obj));
|
||||
}
|
||||
|
||||
public static void requestSave(ConfigObject obj) {
|
||||
if (obj == null || obj instanceof HudGlobalConfig) return;
|
||||
PendingWrite write;
|
||||
ConfigSaveScheduler.PendingWrite write;
|
||||
try {
|
||||
write = buildWrite(obj);
|
||||
} catch (Exception e) {
|
||||
DebugLog.error("Failed to serialize config %s", e, fileOf(obj).toAbsolutePath());
|
||||
DebugLog.error("Failed to serialize config %s", e, ConfigFilePaths.fileOf(obj).toAbsolutePath());
|
||||
return;
|
||||
}
|
||||
if (write == null) return;
|
||||
|
||||
long token;
|
||||
synchronized (SAVE_LOCK) {
|
||||
token = ++SAVE_TOKEN_SEQ;
|
||||
PENDING_TEXT.put(write.path, write.text);
|
||||
PENDING_TOKENS.put(write.path, token);
|
||||
|
||||
ScheduledFuture<?> prev = PENDING_TASKS.get(write.path);
|
||||
if (prev != null) prev.cancel(false);
|
||||
|
||||
ScheduledFuture<?> task = SAVE_EXECUTOR.schedule(
|
||||
() -> flushPending(write.path, token),
|
||||
SAVE_DEBOUNCE_MS,
|
||||
TimeUnit.MILLISECONDS
|
||||
);
|
||||
PENDING_TASKS.put(write.path, task);
|
||||
}
|
||||
ConfigSaveScheduler.schedule(write);
|
||||
}
|
||||
|
||||
public static void load(ConfigObject obj) {
|
||||
if (obj instanceof HudGlobalConfig) return;
|
||||
if (isModule(obj)) {
|
||||
loadModule(obj);
|
||||
} else if (isJsonConfig(obj)) {
|
||||
loadJson(obj);
|
||||
} else {
|
||||
loadJson5(obj);
|
||||
}
|
||||
if (isModule(obj)) ConfigGsonSupport.loadModule(obj);
|
||||
else if (isJsonConfig(obj)) ConfigGsonSupport.loadJson(obj);
|
||||
else ConfigJson5Support.loadJson5(obj);
|
||||
}
|
||||
|
||||
private static boolean isModule(ConfigObject obj) {
|
||||
return obj instanceof Module;
|
||||
}
|
||||
private static boolean isModule(ConfigObject obj) { return obj instanceof Module; }
|
||||
private static boolean isJsonConfig(ConfigObject obj) { return obj instanceof JsonConfigObject; }
|
||||
|
||||
private static boolean isJsonConfig(ConfigObject obj) {
|
||||
return obj instanceof JsonConfigObject;
|
||||
}
|
||||
|
||||
private static String configNameOf(ConfigObject obj) {
|
||||
String name = null;
|
||||
if (obj instanceof ConfigNameProvider provider) {
|
||||
name = provider.getConfigName();
|
||||
}
|
||||
if (name == null || name.isBlank()) {
|
||||
name = obj.getClass().getSimpleName();
|
||||
}
|
||||
return sanitizeConfigName(name);
|
||||
}
|
||||
|
||||
private static String sanitizeConfigName(String name) {
|
||||
if (name == null) return "config";
|
||||
String base = name.trim().toLowerCase(Locale.ROOT);
|
||||
if (base.isEmpty()) return "config";
|
||||
StringBuilder out = new StringBuilder(base.length());
|
||||
for (int i = 0; i < base.length(); i++) {
|
||||
char c = base.charAt(i);
|
||||
if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.') {
|
||||
out.append(c);
|
||||
} else {
|
||||
out.append('_');
|
||||
}
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private static Path namedJsonFile(String configName) {
|
||||
return CONFIG_DIR.resolve(sanitizeConfigName(configName) + ".json");
|
||||
}
|
||||
|
||||
private static Path namedJson5File(String configName) {
|
||||
return CONFIG_DIR.resolve(sanitizeConfigName(configName) + ".json5");
|
||||
}
|
||||
|
||||
private static Path moduleFile(ConfigObject obj) {
|
||||
String category = "uncategorized";
|
||||
if (obj instanceof Module module && module.getCategory() != null) {
|
||||
category = sanitizeConfigName(module.getCategory().name());
|
||||
}
|
||||
return CONFIG_DIR.resolve("modules")
|
||||
.resolve(category)
|
||||
.resolve(configNameOf(obj) + ".json");
|
||||
}
|
||||
|
||||
private static Path jsonFile(ConfigObject obj) {
|
||||
if (obj instanceof DraggableHudElement) {
|
||||
return CONFIG_DIR.resolve("hud")
|
||||
.resolve("draggable")
|
||||
.resolve(configNameOf(obj) + ".json");
|
||||
}
|
||||
if (obj instanceof AbstractHudElement) {
|
||||
return CONFIG_DIR.resolve("hud")
|
||||
.resolve("nondraggable")
|
||||
.resolve(configNameOf(obj) + ".json");
|
||||
}
|
||||
return namedJsonFile(configNameOf(obj));
|
||||
}
|
||||
|
||||
private static Path utilFile(ConfigObject obj) {
|
||||
return isJsonConfig(obj)
|
||||
? jsonFile(obj)
|
||||
: namedJson5File(configNameOf(obj));
|
||||
}
|
||||
|
||||
private static Path fileOf(ConfigObject obj) {
|
||||
return isModule(obj) ? moduleFile(obj) : utilFile(obj);
|
||||
}
|
||||
|
||||
// =============================================================
|
||||
// Module (Gson JSON)
|
||||
// =============================================================
|
||||
|
||||
private static PendingWrite buildModuleWrite(ConfigObject obj) {
|
||||
try {
|
||||
JsonObject root = new JsonObject();
|
||||
for (ConfigValue<?> v : obj.getConfigValues()) {
|
||||
root.add(v.getName(), toJsonElement(v.toJson()));
|
||||
}
|
||||
|
||||
String text = GSON.toJson(root) + "\n";
|
||||
Path out = moduleFile(obj);
|
||||
return new PendingWrite(out, text);
|
||||
} catch (Exception e) {
|
||||
DebugLog.error("Failed to serialize config %s", e, fileOf(obj).toAbsolutePath());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void loadModule(ConfigObject obj) {
|
||||
try {
|
||||
Path path = moduleFile(obj);
|
||||
|
||||
if (!Files.exists(path)) {
|
||||
DebugLog.config("Config defaults created: %s", path.toAbsolutePath());
|
||||
save(obj);
|
||||
return;
|
||||
}
|
||||
|
||||
DebugLog.config("Config loading: %s", path.toAbsolutePath());
|
||||
|
||||
String txt = Files.readString(path);
|
||||
JsonElement parsed = GSON.fromJson(txt, JsonElement.class);
|
||||
if (!(parsed instanceof JsonObject root)) return;
|
||||
|
||||
Module module = (Module) obj;
|
||||
|
||||
JsonElement legacySettings = root.get("settings");
|
||||
if (legacySettings != null) {
|
||||
module.applyLegacySettings(unwrapJsonElement(legacySettings));
|
||||
}
|
||||
|
||||
boolean usedLegacyAlias = false;
|
||||
for (ConfigValue<?> v : obj.getConfigValues()) {
|
||||
JsonLookup lookup = findJsonElement(obj, root, v.getName());
|
||||
if (lookup.element() != null) {
|
||||
v.fromJson(unwrapJsonElement(lookup.element()));
|
||||
usedLegacyAlias |= lookup.alias();
|
||||
}
|
||||
}
|
||||
|
||||
module.syncSettingsAfterLoad();
|
||||
if (usedLegacyAlias) {
|
||||
requestSave(obj);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
DebugLog.error("Failed to load config %s", e, fileOf(obj).toAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================
|
||||
// JSON (Gson) for non-Module configs
|
||||
// =============================================================
|
||||
|
||||
private static PendingWrite buildJsonWrite(ConfigObject obj) {
|
||||
try {
|
||||
JsonObject root = new JsonObject();
|
||||
for (ConfigValue<?> v : obj.getConfigValues()) {
|
||||
root.add(v.getName(), toJsonElement(v.toJson()));
|
||||
}
|
||||
|
||||
String text = GSON.toJson(root) + "\n";
|
||||
Path out = jsonFile(obj);
|
||||
return new PendingWrite(out, text);
|
||||
} catch (Exception e) {
|
||||
DebugLog.error("Failed to serialize config %s", e, fileOf(obj).toAbsolutePath());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void loadJson(ConfigObject obj) {
|
||||
try {
|
||||
Path path = jsonFile(obj);
|
||||
if (!Files.exists(path)) {
|
||||
DebugLog.config("Config defaults created: %s", path.toAbsolutePath());
|
||||
save(obj);
|
||||
return;
|
||||
}
|
||||
|
||||
DebugLog.config("Config loading: %s", path.toAbsolutePath());
|
||||
|
||||
String txt = Files.readString(path);
|
||||
JsonElement parsed = GSON.fromJson(txt, JsonElement.class);
|
||||
if (!(parsed instanceof JsonObject root)) return;
|
||||
|
||||
boolean missing = false;
|
||||
boolean usedLegacyAlias = false;
|
||||
for (ConfigValue<?> v : obj.getConfigValues()) {
|
||||
JsonLookup lookup = findJsonElement(obj, root, v.getName());
|
||||
if (lookup.element() != null) {
|
||||
v.fromJson(unwrapJsonElement(lookup.element()));
|
||||
usedLegacyAlias |= lookup.alias();
|
||||
} else {
|
||||
missing = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (missing || usedLegacyAlias) {
|
||||
requestSave(obj);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
DebugLog.error("Failed to load config %s", e, fileOf(obj).toAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================
|
||||
// Utilities (Json5)
|
||||
// =============================================================
|
||||
|
||||
private static PendingWrite buildJson5Write(ConfigObject obj) {
|
||||
try {
|
||||
Json5Object root = new Json5Object();
|
||||
|
||||
for (ConfigValue<?> v : obj.getConfigValues()) {
|
||||
Object raw = v.toJson();
|
||||
root.add(v.getName(), toJson5Element(raw));
|
||||
}
|
||||
|
||||
String text = JSON5.serialize(root);
|
||||
|
||||
String sb = text + "\n";
|
||||
|
||||
Path out = utilFile(obj);
|
||||
if (obj instanceof EntityFilters) {
|
||||
DebugLog.config("[EntityFilters][SAVE] to=%s data=%s", out.toAbsolutePath(), text);
|
||||
}
|
||||
return new PendingWrite(out, sb);
|
||||
} catch (Exception e) {
|
||||
DebugLog.error("Failed to serialize config %s", e, fileOf(obj).toAbsolutePath());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void loadJson5(ConfigObject obj) {
|
||||
try {
|
||||
Path path = utilFile(obj);
|
||||
if (!Files.exists(path)) {
|
||||
DebugLog.config("Config defaults created: %s", path.toAbsolutePath());
|
||||
save(obj);
|
||||
return;
|
||||
}
|
||||
|
||||
DebugLog.config("Config loading: %s", path.toAbsolutePath());
|
||||
|
||||
String txt = Files.readString(path);
|
||||
Json5Element parsed = JSON5.parse(txt);
|
||||
|
||||
if (!(parsed instanceof Json5Object root)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Json5Element> map = root.asMap();
|
||||
|
||||
boolean missing = false;
|
||||
boolean usedLegacyAlias = false;
|
||||
for (ConfigValue<?> v : obj.getConfigValues()) {
|
||||
Json5Lookup lookup = findJson5Element(obj, map, v.getName());
|
||||
if (lookup.element() != null) {
|
||||
v.fromJson(unwrapJson5(lookup.element()));
|
||||
usedLegacyAlias |= lookup.alias();
|
||||
} else {
|
||||
missing = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (obj instanceof Module module) {
|
||||
module.syncSettingsAfterLoad();
|
||||
}
|
||||
if (missing || usedLegacyAlias) {
|
||||
requestSave(obj);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
DebugLog.error("Failed to load config %s", e, fileOf(obj).toAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
private static JsonLookup findJsonElement(ConfigObject obj, JsonObject root, String valueName) {
|
||||
if (root == null || valueName == null) return JsonLookup.missing();
|
||||
JsonElement direct = root.get(valueName);
|
||||
if (direct != null && !direct.isJsonNull()) {
|
||||
return new JsonLookup(direct, false);
|
||||
}
|
||||
if (obj instanceof ConfigValueAliasProvider provider) {
|
||||
List<String> aliases = provider.getLegacyValueNames(valueName);
|
||||
if (aliases != null) {
|
||||
for (String alias : aliases) {
|
||||
if (alias == null || alias.isBlank()) continue;
|
||||
JsonElement legacy = root.get(alias);
|
||||
if (legacy != null && !legacy.isJsonNull()) {
|
||||
return new JsonLookup(legacy, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return JsonLookup.missing();
|
||||
}
|
||||
|
||||
private static Json5Lookup findJson5Element(ConfigObject obj, Map<String, Json5Element> map, String valueName) {
|
||||
if (map == null || valueName == null) return Json5Lookup.missing();
|
||||
Json5Element direct = map.get(valueName);
|
||||
if (direct != null) {
|
||||
return new Json5Lookup(direct, false);
|
||||
}
|
||||
if (obj instanceof ConfigValueAliasProvider provider) {
|
||||
List<String> aliases = provider.getLegacyValueNames(valueName);
|
||||
if (aliases != null) {
|
||||
for (String alias : aliases) {
|
||||
if (alias == null || alias.isBlank()) continue;
|
||||
Json5Element legacy = map.get(alias);
|
||||
if (legacy != null) {
|
||||
return new Json5Lookup(legacy, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Json5Lookup.missing();
|
||||
}
|
||||
|
||||
private static JsonElement toJsonElement(Object v) {
|
||||
if (v == null) return JsonNull.INSTANCE;
|
||||
if (v instanceof JsonElement el) return el;
|
||||
if (v instanceof Boolean b) return new JsonPrimitive(b);
|
||||
if (v instanceof Number n) return new JsonPrimitive(n);
|
||||
if (v instanceof CharSequence s) return new JsonPrimitive(s.toString());
|
||||
if (v instanceof Map<?, ?> map) {
|
||||
JsonObject obj = new JsonObject();
|
||||
for (var e : map.entrySet()) {
|
||||
obj.add(String.valueOf(e.getKey()), toJsonElement(e.getValue()));
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
if (v instanceof Iterable<?> it) {
|
||||
JsonArray arr = new JsonArray();
|
||||
for (Object el : it) arr.add(toJsonElement(el));
|
||||
return arr;
|
||||
}
|
||||
return new JsonPrimitive(String.valueOf(v));
|
||||
}
|
||||
|
||||
private static Json5Element toJson5Element(Object v) {
|
||||
if (v == null) {
|
||||
return Json5Primitive.fromNull();
|
||||
}
|
||||
if (v instanceof Json5Element el) {
|
||||
return el;
|
||||
}
|
||||
if (v instanceof Boolean b) {
|
||||
return Json5Primitive.fromBoolean(b);
|
||||
}
|
||||
if (v instanceof Number n) {
|
||||
return Json5Primitive.fromNumber(n);
|
||||
}
|
||||
if (v instanceof CharSequence s) {
|
||||
return Json5Primitive.fromString(s.toString());
|
||||
}
|
||||
if (v instanceof Map<?, ?> map) {
|
||||
Json5Object obj = new Json5Object();
|
||||
for (var e : map.entrySet()) {
|
||||
String key = String.valueOf(e.getKey());
|
||||
obj.add(key, toJson5Element(e.getValue()));
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
if (v instanceof Iterable<?> it) {
|
||||
Json5Array arr = new Json5Array();
|
||||
for (Object el : it) {
|
||||
arr.add(toJson5Element(el));
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
return Json5Primitive.fromString(String.valueOf(v));
|
||||
}
|
||||
|
||||
// =============================================================
|
||||
// Helpers
|
||||
// =============================================================
|
||||
|
||||
private static Object unwrapJsonElement(JsonElement el) {
|
||||
if (el == null || el.isJsonNull()) return null;
|
||||
if (el.isJsonPrimitive()) {
|
||||
JsonPrimitive p = el.getAsJsonPrimitive();
|
||||
if (p.isBoolean()) return p.getAsBoolean();
|
||||
if (p.isNumber()) return p.getAsNumber();
|
||||
return p.getAsString();
|
||||
}
|
||||
if (el.isJsonArray()) {
|
||||
List<Object> out = new ArrayList<>();
|
||||
for (JsonElement child : el.getAsJsonArray()) {
|
||||
out.add(unwrapJsonElement(child));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
if (el.isJsonObject()) {
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
for (var e : el.getAsJsonObject().entrySet()) {
|
||||
out.put(e.getKey(), unwrapJsonElement(e.getValue()));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Object unwrapJson5(Json5Element el) {
|
||||
if (el == null || el.isJson5Null()) return null;
|
||||
if (el.isJson5Primitive()) {
|
||||
Json5Primitive p = el.getAsJson5Primitive();
|
||||
if (p.isBoolean()) return p.getAsBoolean();
|
||||
if (p.isNumber()) return p.getAsNumber();
|
||||
return p.getAsString();
|
||||
}
|
||||
if (el.isJson5Array()) {
|
||||
List<Object> out = new ArrayList<>();
|
||||
for (Json5Element child : el.getAsJson5Array()) {
|
||||
out.add(unwrapJson5(child));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
if (el.isJson5Object()) {
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
for (var e : el.getAsJson5Object().asMap().entrySet()) {
|
||||
out.put(e.getKey(), unwrapJson5(e.getValue()));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static PendingWrite buildWrite(ConfigObject obj) {
|
||||
private static ConfigSaveScheduler.PendingWrite buildWrite(ConfigObject obj) {
|
||||
if (obj == null) return null;
|
||||
if (isModule(obj)) return buildModuleWrite(obj);
|
||||
if (isJsonConfig(obj)) return buildJsonWrite(obj);
|
||||
return buildJson5Write(obj);
|
||||
}
|
||||
|
||||
private static void saveNow(ConfigObject obj) {
|
||||
PendingWrite write;
|
||||
try {
|
||||
write = buildWrite(obj);
|
||||
} catch (Exception e) {
|
||||
DebugLog.error("Failed to serialize config %s", e, fileOf(obj).toAbsolutePath());
|
||||
return;
|
||||
}
|
||||
if (write == null) return;
|
||||
clearPending(write.path);
|
||||
writeConfig(write.path, write.text);
|
||||
}
|
||||
|
||||
// =============================================================
|
||||
// Async write helpers
|
||||
// =============================================================
|
||||
|
||||
private static void flushPending(Path path, long token) {
|
||||
String text;
|
||||
synchronized (SAVE_LOCK) {
|
||||
Long current = PENDING_TOKENS.get(path);
|
||||
if (current == null || current != token) return;
|
||||
text = PENDING_TEXT.remove(path);
|
||||
PENDING_TOKENS.remove(path);
|
||||
PENDING_TASKS.remove(path);
|
||||
}
|
||||
if (text != null) {
|
||||
writeConfig(path, text);
|
||||
}
|
||||
}
|
||||
|
||||
private static void clearPending(Path path) {
|
||||
synchronized (SAVE_LOCK) {
|
||||
ScheduledFuture<?> prev = PENDING_TASKS.remove(path);
|
||||
if (prev != null) prev.cancel(false);
|
||||
PENDING_TEXT.remove(path);
|
||||
PENDING_TOKENS.remove(path);
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeConfig(Path out, String text) {
|
||||
try {
|
||||
Path parent = out.getParent();
|
||||
Files.createDirectories(parent != null ? parent : CONFIG_DIR);
|
||||
Files.writeString(out, text);
|
||||
DebugLog.config("Config saved: %s", out.toAbsolutePath());
|
||||
} catch (IOException e) {
|
||||
DebugLog.error("Failed to save config %s", e, out.toAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
private record JsonLookup(JsonElement element, boolean alias) {
|
||||
static JsonLookup missing() {
|
||||
return new JsonLookup(null, false);
|
||||
}
|
||||
}
|
||||
|
||||
private record Json5Lookup(Json5Element element, boolean alias) {
|
||||
static Json5Lookup missing() {
|
||||
return new Json5Lookup(null, false);
|
||||
}
|
||||
}
|
||||
|
||||
private record PendingWrite(Path path, String text) {
|
||||
if (isModule(obj)) return ConfigGsonSupport.buildModuleWrite(obj);
|
||||
if (isJsonConfig(obj)) return ConfigGsonSupport.buildJsonWrite(obj);
|
||||
return ConfigJson5Support.buildJson5Write(obj);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.io;
|
||||
import dev.loki.lovisual.config.definition.ConfigObject;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.io;
|
||||
|
||||
import java.util.List;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
package dev.loki.lovisual.config.io;
|
||||
|
||||
import dev.loki.lovisual.config.definition.ConfigNameProvider;
|
||||
import dev.loki.lovisual.config.definition.ConfigObject;
|
||||
import dev.loki.lovisual.features.gui.hud.base.AbstractHudElement;
|
||||
import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement;
|
||||
import dev.loki.lovisual.features.module.core.Module;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
|
||||
final class ConfigFilePaths {
|
||||
private static final Path CONFIG_DIR = ConfigPaths.root();
|
||||
|
||||
private ConfigFilePaths() {}
|
||||
|
||||
static Path fileOf(ConfigObject obj) {
|
||||
return isModule(obj) ? moduleFile(obj) : utilFile(obj);
|
||||
}
|
||||
|
||||
static Path moduleFile(ConfigObject obj) {
|
||||
String category = "uncategorized";
|
||||
if (obj instanceof Module module && module.getCategory() != null) {
|
||||
category = sanitizeConfigName(module.getCategory().name());
|
||||
}
|
||||
return CONFIG_DIR.resolve("modules")
|
||||
.resolve(category)
|
||||
.resolve(configNameOf(obj) + ".json");
|
||||
}
|
||||
|
||||
static Path jsonFile(ConfigObject obj) {
|
||||
if (obj instanceof DraggableHudElement) {
|
||||
return CONFIG_DIR.resolve("hud")
|
||||
.resolve("draggable")
|
||||
.resolve(configNameOf(obj) + ".json");
|
||||
}
|
||||
if (obj instanceof AbstractHudElement) {
|
||||
return CONFIG_DIR.resolve("hud")
|
||||
.resolve("nondraggable")
|
||||
.resolve(configNameOf(obj) + ".json");
|
||||
}
|
||||
return namedJsonFile(configNameOf(obj));
|
||||
}
|
||||
|
||||
static Path utilFile(ConfigObject obj) {
|
||||
return isJsonConfig(obj) ? jsonFile(obj) : namedJson5File(configNameOf(obj));
|
||||
}
|
||||
|
||||
static String configNameOf(ConfigObject obj) {
|
||||
String name = null;
|
||||
if (obj instanceof ConfigNameProvider provider) {
|
||||
name = provider.getConfigName();
|
||||
}
|
||||
if (name == null || name.isBlank()) {
|
||||
name = obj.getClass().getSimpleName();
|
||||
}
|
||||
return sanitizeConfigName(name);
|
||||
}
|
||||
|
||||
static String sanitizeConfigName(String name) {
|
||||
if (name == null) return "config";
|
||||
String base = name.trim().toLowerCase(Locale.ROOT);
|
||||
if (base.isEmpty()) return "config";
|
||||
StringBuilder out = new StringBuilder(base.length());
|
||||
for (int i = 0; i < base.length(); i++) {
|
||||
char c = base.charAt(i);
|
||||
if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.') {
|
||||
out.append(c);
|
||||
} else {
|
||||
out.append('_');
|
||||
}
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
static Path namedJsonFile(String configName) {
|
||||
return CONFIG_DIR.resolve(sanitizeConfigName(configName) + ".json");
|
||||
}
|
||||
|
||||
static Path namedJson5File(String configName) {
|
||||
return CONFIG_DIR.resolve(sanitizeConfigName(configName) + ".json5");
|
||||
}
|
||||
|
||||
static Path rootDir() {
|
||||
return CONFIG_DIR;
|
||||
}
|
||||
|
||||
private static boolean isModule(ConfigObject obj) {
|
||||
return obj instanceof Module;
|
||||
}
|
||||
|
||||
private static boolean isJsonConfig(ConfigObject obj) {
|
||||
return obj instanceof JsonConfigObject;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
package dev.loki.lovisual.config.io;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonNull;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import dev.loki.lovisual.config.definition.ConfigObject;
|
||||
import dev.loki.lovisual.config.i18n.ConfigValueAliasProvider;
|
||||
import dev.loki.lovisual.config.values.collection.ConfigValue;
|
||||
import dev.loki.lovisual.features.module.core.Module;
|
||||
import dev.loki.lovisual.util.logging.DebugLog;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
final class ConfigGsonSupport {
|
||||
static final Gson GSON = new GsonBuilder()
|
||||
.setPrettyPrinting()
|
||||
.disableHtmlEscaping()
|
||||
.create();
|
||||
|
||||
private ConfigGsonSupport() {}
|
||||
|
||||
static ConfigSaveScheduler.PendingWrite buildModuleWrite(ConfigObject obj) {
|
||||
try {
|
||||
JsonObject root = new JsonObject();
|
||||
for (ConfigValue<?> v : obj.getConfigValues()) {
|
||||
root.add(v.getName(), toJsonElement(v.toJson()));
|
||||
}
|
||||
String text = GSON.toJson(root) + "\n";
|
||||
Path out = ConfigFilePaths.moduleFile(obj);
|
||||
return new ConfigSaveScheduler.PendingWrite(out, text);
|
||||
} catch (Exception e) {
|
||||
DebugLog.error("Failed to serialize config %s", e, ConfigFilePaths.fileOf(obj).toAbsolutePath());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static ConfigSaveScheduler.PendingWrite buildJsonWrite(ConfigObject obj) {
|
||||
try {
|
||||
JsonObject root = new JsonObject();
|
||||
for (ConfigValue<?> v : obj.getConfigValues()) {
|
||||
root.add(v.getName(), toJsonElement(v.toJson()));
|
||||
}
|
||||
String text = GSON.toJson(root) + "\n";
|
||||
Path out = ConfigFilePaths.jsonFile(obj);
|
||||
return new ConfigSaveScheduler.PendingWrite(out, text);
|
||||
} catch (Exception e) {
|
||||
DebugLog.error("Failed to serialize config %s", e, ConfigFilePaths.fileOf(obj).toAbsolutePath());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static void loadModule(ConfigObject obj) {
|
||||
try {
|
||||
Path path = ConfigFilePaths.moduleFile(obj);
|
||||
if (!Files.exists(path)) {
|
||||
DebugLog.config("Config defaults created: %s", path.toAbsolutePath());
|
||||
ConfigSerializer.save(obj);
|
||||
return;
|
||||
}
|
||||
DebugLog.config("Config loading: %s", path.toAbsolutePath());
|
||||
String txt = Files.readString(path);
|
||||
JsonElement parsed = GSON.fromJson(txt, JsonElement.class);
|
||||
if (!(parsed instanceof JsonObject root)) return;
|
||||
Module module = (Module) obj;
|
||||
JsonElement legacySettings = root.get("settings");
|
||||
if (legacySettings != null) {
|
||||
module.applyLegacySettings(unwrapJsonElement(legacySettings));
|
||||
}
|
||||
boolean usedLegacyAlias = false;
|
||||
for (ConfigValue<?> v : obj.getConfigValues()) {
|
||||
JsonLookup lookup = findJsonElement(obj, root, v.getName());
|
||||
if (lookup.element() != null) {
|
||||
v.fromJson(unwrapJsonElement(lookup.element()));
|
||||
usedLegacyAlias |= lookup.alias();
|
||||
}
|
||||
}
|
||||
module.syncSettingsAfterLoad();
|
||||
if (usedLegacyAlias) ConfigSerializer.requestSave(obj);
|
||||
} catch (Exception e) {
|
||||
DebugLog.error("Failed to load config %s", e, ConfigFilePaths.fileOf(obj).toAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
static void loadJson(ConfigObject obj) {
|
||||
try {
|
||||
Path path = ConfigFilePaths.jsonFile(obj);
|
||||
if (!Files.exists(path)) {
|
||||
DebugLog.config("Config defaults created: %s", path.toAbsolutePath());
|
||||
ConfigSerializer.save(obj);
|
||||
return;
|
||||
}
|
||||
DebugLog.config("Config loading: %s", path.toAbsolutePath());
|
||||
String txt = Files.readString(path);
|
||||
JsonElement parsed = GSON.fromJson(txt, JsonElement.class);
|
||||
if (!(parsed instanceof JsonObject root)) return;
|
||||
boolean missing = false;
|
||||
boolean usedLegacyAlias = false;
|
||||
for (ConfigValue<?> v : obj.getConfigValues()) {
|
||||
JsonLookup lookup = findJsonElement(obj, root, v.getName());
|
||||
if (lookup.element() != null) {
|
||||
v.fromJson(unwrapJsonElement(lookup.element()));
|
||||
usedLegacyAlias |= lookup.alias();
|
||||
} else {
|
||||
missing = true;
|
||||
}
|
||||
}
|
||||
if (missing || usedLegacyAlias) ConfigSerializer.requestSave(obj);
|
||||
} catch (Exception e) {
|
||||
DebugLog.error("Failed to load config %s", e, ConfigFilePaths.fileOf(obj).toAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
static JsonElement toJsonElement(Object v) {
|
||||
if (v == null) return JsonNull.INSTANCE;
|
||||
if (v instanceof JsonElement el) return el;
|
||||
if (v instanceof Boolean b) return new JsonPrimitive(b);
|
||||
if (v instanceof Number n) return new JsonPrimitive(n);
|
||||
if (v instanceof CharSequence s) return new JsonPrimitive(s.toString());
|
||||
if (v instanceof Map<?, ?> map) {
|
||||
JsonObject obj = new JsonObject();
|
||||
for (var e : map.entrySet()) obj.add(String.valueOf(e.getKey()), toJsonElement(e.getValue()));
|
||||
return obj;
|
||||
}
|
||||
if (v instanceof Iterable<?> it) {
|
||||
JsonArray arr = new JsonArray();
|
||||
for (Object el : it) arr.add(toJsonElement(el));
|
||||
return arr;
|
||||
}
|
||||
return new JsonPrimitive(String.valueOf(v));
|
||||
}
|
||||
|
||||
static Object unwrapJsonElement(JsonElement el) {
|
||||
if (el == null || el.isJsonNull()) return null;
|
||||
if (el.isJsonPrimitive()) {
|
||||
JsonPrimitive p = el.getAsJsonPrimitive();
|
||||
if (p.isBoolean()) return p.getAsBoolean();
|
||||
if (p.isNumber()) return p.getAsNumber();
|
||||
return p.getAsString();
|
||||
}
|
||||
if (el.isJsonArray()) {
|
||||
List<Object> out = new ArrayList<>();
|
||||
for (JsonElement child : el.getAsJsonArray()) out.add(unwrapJsonElement(child));
|
||||
return out;
|
||||
}
|
||||
if (el.isJsonObject()) {
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
for (var e : el.getAsJsonObject().entrySet()) out.put(e.getKey(), unwrapJsonElement(e.getValue()));
|
||||
return out;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static JsonLookup findJsonElement(ConfigObject obj, JsonObject root, String valueName) {
|
||||
if (root == null || valueName == null) return JsonLookup.missing();
|
||||
JsonElement direct = root.get(valueName);
|
||||
if (direct != null && !direct.isJsonNull()) return new JsonLookup(direct, false);
|
||||
if (obj instanceof ConfigValueAliasProvider provider) {
|
||||
List<String> aliases = provider.getLegacyValueNames(valueName);
|
||||
if (aliases != null) {
|
||||
for (String alias : aliases) {
|
||||
if (alias == null || alias.isBlank()) continue;
|
||||
JsonElement legacy = root.get(alias);
|
||||
if (legacy != null && !legacy.isJsonNull()) return new JsonLookup(legacy, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
return JsonLookup.missing();
|
||||
}
|
||||
|
||||
record JsonLookup(JsonElement element, boolean alias) {
|
||||
static JsonLookup missing() { return new JsonLookup(null, false); }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
package dev.loki.lovisual.config.io;
|
||||
|
||||
import de.marhali.json5.Json5;
|
||||
import de.marhali.json5.Json5Array;
|
||||
import de.marhali.json5.Json5Element;
|
||||
import de.marhali.json5.Json5Object;
|
||||
import de.marhali.json5.Json5Primitive;
|
||||
import dev.loki.lovisual.config.definition.ConfigObject;
|
||||
import dev.loki.lovisual.config.i18n.ConfigValueAliasProvider;
|
||||
import dev.loki.lovisual.config.values.collection.ConfigValue;
|
||||
import dev.loki.lovisual.features.module.core.Module;
|
||||
import dev.loki.lovisual.features.relations.EntityFilters;
|
||||
import dev.loki.lovisual.util.logging.DebugLog;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
final class ConfigJson5Support {
|
||||
static final Json5 JSON5 = Json5.builder(builder -> builder
|
||||
.trailingComma()
|
||||
.indentFactor(2)
|
||||
.quoteSingle()
|
||||
.build());
|
||||
|
||||
private ConfigJson5Support() {}
|
||||
|
||||
static ConfigSaveScheduler.PendingWrite buildJson5Write(ConfigObject obj) {
|
||||
try {
|
||||
Json5Object root = new Json5Object();
|
||||
for (ConfigValue<?> v : obj.getConfigValues()) {
|
||||
root.add(v.getName(), toJson5Element(v.toJson()));
|
||||
}
|
||||
String text = JSON5.serialize(root);
|
||||
String sb = text + "\n";
|
||||
Path out = ConfigFilePaths.utilFile(obj);
|
||||
if (obj instanceof EntityFilters) {
|
||||
DebugLog.config("[EntityFilters][SAVE] to=%s data=%s", out.toAbsolutePath(), text);
|
||||
}
|
||||
return new ConfigSaveScheduler.PendingWrite(out, sb);
|
||||
} catch (Exception e) {
|
||||
DebugLog.error("Failed to serialize config %s", e, ConfigFilePaths.fileOf(obj).toAbsolutePath());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static void loadJson5(ConfigObject obj) {
|
||||
try {
|
||||
Path path = ConfigFilePaths.utilFile(obj);
|
||||
if (!Files.exists(path)) {
|
||||
DebugLog.config("Config defaults created: %s", path.toAbsolutePath());
|
||||
ConfigSerializer.save(obj);
|
||||
return;
|
||||
}
|
||||
DebugLog.config("Config loading: %s", path.toAbsolutePath());
|
||||
String txt = Files.readString(path);
|
||||
Json5Element parsed = JSON5.parse(txt);
|
||||
if (!(parsed instanceof Json5Object root)) return;
|
||||
Map<String, Json5Element> map = root.asMap();
|
||||
boolean missing = false;
|
||||
boolean usedLegacyAlias = false;
|
||||
for (ConfigValue<?> v : obj.getConfigValues()) {
|
||||
Json5Lookup lookup = findJson5Element(obj, map, v.getName());
|
||||
if (lookup.element() != null) {
|
||||
v.fromJson(unwrapJson5(lookup.element()));
|
||||
usedLegacyAlias |= lookup.alias();
|
||||
} else {
|
||||
missing = true;
|
||||
}
|
||||
}
|
||||
if (obj instanceof Module module) module.syncSettingsAfterLoad();
|
||||
if (missing || usedLegacyAlias) ConfigSerializer.requestSave(obj);
|
||||
} catch (Exception e) {
|
||||
DebugLog.error("Failed to load config %s", e, ConfigFilePaths.fileOf(obj).toAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
static Json5Element toJson5Element(Object v) {
|
||||
if (v == null) return Json5Primitive.fromNull();
|
||||
if (v instanceof Json5Element el) return el;
|
||||
if (v instanceof Boolean b) return Json5Primitive.fromBoolean(b);
|
||||
if (v instanceof Number n) return Json5Primitive.fromNumber(n);
|
||||
if (v instanceof CharSequence s) return Json5Primitive.fromString(s.toString());
|
||||
if (v instanceof Map<?, ?> map) {
|
||||
Json5Object obj = new Json5Object();
|
||||
for (var e : map.entrySet()) obj.add(String.valueOf(e.getKey()), toJson5Element(e.getValue()));
|
||||
return obj;
|
||||
}
|
||||
if (v instanceof Iterable<?> it) {
|
||||
Json5Array arr = new Json5Array();
|
||||
for (Object el : it) arr.add(toJson5Element(el));
|
||||
return arr;
|
||||
}
|
||||
return Json5Primitive.fromString(String.valueOf(v));
|
||||
}
|
||||
|
||||
static Object unwrapJson5(Json5Element el) {
|
||||
if (el == null || el.isJson5Null()) return null;
|
||||
if (el.isJson5Primitive()) {
|
||||
Json5Primitive p = el.getAsJson5Primitive();
|
||||
if (p.isBoolean()) return p.getAsBoolean();
|
||||
if (p.isNumber()) return p.getAsNumber();
|
||||
return p.getAsString();
|
||||
}
|
||||
if (el.isJson5Array()) {
|
||||
List<Object> out = new ArrayList<>();
|
||||
for (Json5Element child : el.getAsJson5Array()) out.add(unwrapJson5(child));
|
||||
return out;
|
||||
}
|
||||
if (el.isJson5Object()) {
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
for (var e : el.getAsJson5Object().asMap().entrySet()) out.put(e.getKey(), unwrapJson5(e.getValue()));
|
||||
return out;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Json5Lookup findJson5Element(ConfigObject obj, Map<String, Json5Element> map, String valueName) {
|
||||
if (map == null || valueName == null) return Json5Lookup.missing();
|
||||
Json5Element direct = map.get(valueName);
|
||||
if (direct != null) return new Json5Lookup(direct, false);
|
||||
if (obj instanceof ConfigValueAliasProvider provider) {
|
||||
List<String> aliases = provider.getLegacyValueNames(valueName);
|
||||
if (aliases != null) {
|
||||
for (String alias : aliases) {
|
||||
if (alias == null || alias.isBlank()) continue;
|
||||
Json5Element legacy = map.get(alias);
|
||||
if (legacy != null) return new Json5Lookup(legacy, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Json5Lookup.missing();
|
||||
}
|
||||
|
||||
record Json5Lookup(Json5Element element, boolean alias) {
|
||||
static Json5Lookup missing() { return new Json5Lookup(null, false); }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
package dev.loki.lovisual.config.io;
|
||||
|
||||
import dev.loki.lovisual.util.logging.DebugLog;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
final class ConfigSaveScheduler {
|
||||
private static final long SAVE_DEBOUNCE_MS = 220L;
|
||||
private static final ScheduledExecutorService SAVE_EXECUTOR =
|
||||
Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "LoVisual-ConfigSaver");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
private static final Object SAVE_LOCK = new Object();
|
||||
private static final Map<Path, String> PENDING_TEXT = new HashMap<>();
|
||||
private static final Map<Path, ScheduledFuture<?>> PENDING_TASKS = new HashMap<>();
|
||||
private static final Map<Path, Long> PENDING_TOKENS = new HashMap<>();
|
||||
private static long SAVE_TOKEN_SEQ = 0L;
|
||||
|
||||
private ConfigSaveScheduler() {}
|
||||
|
||||
static void schedule(PendingWrite write) {
|
||||
if (write == null) return;
|
||||
long token;
|
||||
synchronized (SAVE_LOCK) {
|
||||
token = ++SAVE_TOKEN_SEQ;
|
||||
PENDING_TEXT.put(write.path(), write.text());
|
||||
PENDING_TOKENS.put(write.path(), token);
|
||||
ScheduledFuture<?> prev = PENDING_TASKS.get(write.path());
|
||||
if (prev != null) prev.cancel(false);
|
||||
ScheduledFuture<?> task = SAVE_EXECUTOR.schedule(
|
||||
() -> flushPending(write.path(), token),
|
||||
SAVE_DEBOUNCE_MS,
|
||||
TimeUnit.MILLISECONDS
|
||||
);
|
||||
PENDING_TASKS.put(write.path(), task);
|
||||
}
|
||||
}
|
||||
|
||||
static void saveNow(PendingWrite write) {
|
||||
if (write == null) return;
|
||||
clearPending(write.path());
|
||||
writeConfig(write.path(), write.text());
|
||||
}
|
||||
|
||||
private static void flushPending(Path path, long token) {
|
||||
String text;
|
||||
synchronized (SAVE_LOCK) {
|
||||
Long current = PENDING_TOKENS.get(path);
|
||||
if (current == null || current != token) return;
|
||||
text = PENDING_TEXT.remove(path);
|
||||
PENDING_TOKENS.remove(path);
|
||||
PENDING_TASKS.remove(path);
|
||||
}
|
||||
if (text != null) {
|
||||
writeConfig(path, text);
|
||||
}
|
||||
}
|
||||
|
||||
static void clearPending(Path path) {
|
||||
synchronized (SAVE_LOCK) {
|
||||
ScheduledFuture<?> prev = PENDING_TASKS.remove(path);
|
||||
if (prev != null) prev.cancel(false);
|
||||
PENDING_TEXT.remove(path);
|
||||
PENDING_TOKENS.remove(path);
|
||||
}
|
||||
}
|
||||
|
||||
static void writeConfig(Path out, String text) {
|
||||
try {
|
||||
Path parent = out.getParent();
|
||||
Path base = ConfigFilePaths.rootDir();
|
||||
Files.createDirectories(parent != null ? parent : base);
|
||||
Files.writeString(out, text);
|
||||
DebugLog.config("Config saved: %s", out.toAbsolutePath());
|
||||
} catch (IOException e) {
|
||||
DebugLog.error("Failed to save config %s", e, out.toAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
record PendingWrite(Path path, String text) {}
|
||||
}
|
||||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.profile.codec;
|
||||
|
||||
import java.io.*;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.profile.codec;
|
||||
|
||||
import java.util.Collections;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.profile.codec;
|
||||
|
||||
import java.util.Locale;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.profile.codec;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.profile.diff;
|
||||
|
||||
import java.time.Instant;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.profile.diff;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.profile.diff;
|
||||
|
||||
import java.util.*;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.profile.service;
|
||||
|
||||
import dev.loki.lovisual.features.theme.Theme;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.profile.service;
|
||||
|
||||
import dev.loki.lovisual.features.theme.Theme;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.profile.service;
|
||||
|
||||
import java.io.IOException;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.profile.service;
|
||||
|
||||
import dev.loki.lovisual.config.io.ConfigPaths;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.profile.snapshot;
|
||||
import dev.loki.lovisual.config.profile.codec.ConfigProfileBinaryCodec;
|
||||
import dev.loki.lovisual.config.profile.codec.ConfigProfileType;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.profile.snapshot;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.values.collection;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.values.collection;
|
||||
|
||||
import java.util.*;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.values.collection;
|
||||
|
||||
import dev.loki.lovisual.util.logging.DebugLog;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.values.collection;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.values.color;
|
||||
|
||||
public interface ColorValue {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.values.color;
|
||||
import dev.loki.lovisual.config.values.collection.ConfigValue;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.values.color;
|
||||
|
||||
import dev.loki.lovisual.render.engine.color.ColorUtils;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.values.color;
|
||||
|
||||
import dev.loki.lovisual.render.engine.color.ColorUtils;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.values.mode;
|
||||
|
||||
public enum BindMode {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.values.mode;
|
||||
|
||||
import java.util.Locale;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.values.mode;
|
||||
|
||||
import java.util.*;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
/*
|
||||
* 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.config.values.mode;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue